Solo Practice Management

SoloPilot

End Busywork, Get Paid Faster

SoloPilot is a lightweight practice-management SaaS that centralizes scheduling, client notes, invoicing, and automations into one workspace. It helps independent consultants, coaches, therapists, and freelancers stop manual handoffs: one-click session-to-invoice auto-populates notes and billing, reclaiming 6+ billable hours monthly, preventing missed charges, and accelerating payments.

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

SoloPilot

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 solo professionals to reclaim billable hours and run profitable, low-stress practices through seamless admin automation preserving client context.
Long Term Goal
Within 3 years, help 50,000 solo professionals reclaim 5 million billable hours annually, cut invoice-to-payment time by 50%, and prevent missed charges.
Impact
Cuts administrative time for independent consultants, coaches, therapists, and freelancers by 40%, reclaiming an average of 6+ billable hours monthly, increasing on-time payments by 30% and reducing invoice-to-payment time from 14 to 5 days, preventing missed charges and lost revenue.

Problem & Solution

Problem Statement
Independent consultants, coaches, therapists, and freelancers waste billable hours on fragmented admin—scheduling, session notes, and invoicing—because enterprise suites are overkill and standalone apps force manual handoffs, causing missed charges and delayed payments.
Solution Overview
SoloPilot centralizes scheduling, client records, and payments in one workspace to eliminate manual handoffs; a one-click session-to-invoice workflow auto-populates notes and billing, and integrated payments reduce missed charges and accelerate collection.

Details & Audience

Description
SoloPilot is a lightweight practice-management SaaS that centralizes scheduling, invoicing, client notes, and automations for solo professionals. It serves independent consultants, coaches, therapists, and freelancers who hate administrative busywork. It slashes admin time, prevents missed charges, and speeds payments so owners book more and reclaim billable hours. A one-click session-to-invoice workflow auto-populates notes and billing, preserving client context and eliminating manual handoffs.
Target Audience
Independent consultants, coaches, therapists, freelancers (25–55) who hate admin and prioritize billable hours.
Inspiration
While shadowing a freelance coach, I watched them juggle three browser tabs, sticky notes stuck to a laptop, and a tattered spreadsheet to invoice one client; a missing line meant a lost charge and weeks chasing a check. That raw mix of frustration and wasted income sparked SoloPilot: a one-click session-to-invoice flow that preserves client notes, eliminates manual handoffs, and gets helpers paid.

User Personas

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

I

Intake Integrator Imani

- Solo coach/consultant, 3–7 years independent practice - Manages 20–40 active clients per quarter - Tech stack: Google Workspace, Notion, Typeform, Zapier - Based in US/UK, serves remote global clients - Revenue $90k–$180k, invests in automation tools

Background

Started with forms feeding spreadsheets and manual follow-ups that slipped. Built duct-taped Zaps; brittle failures cost leads and time. Now seeks one system that captures intake and triggers the entire workflow.

Needs & Pain Points

Needs

1. Embedded booking with linked pre-session questionnaire 2. Automatic client creation from bookings and messages 3. Post-session notes to invoice in one click

Pain Points

1. Leads disappear between form, email, and calendar 2. Duplicate entry across tools causes errors 3. Missed follow-ups without automated nudges

Psychographics

- Worships clean, repeatable processes - Automation over improvisation, always - Time is currency, hates rework - Professional polish signals trust and expertise

Channels

1. Gmail — daily 2. LinkedIn — outreach 3. Google Calendar — anchor 4. Typeform — intake 5. Zapier — automations

A

Afterhours Arranger Alex

- Part-time consultant/coach, evenings and weekends - 5–10 client sessions weekly, variable cadence - Lives suburban, commutes by train; iPhone-first - Income $40k salary + $35k side revenue - Tech stack: Google Calendar, WhatsApp, Stripe, Notes app

Background

Started consulting after-hours; missed charges when sessions stacked and trains arrived. Tried remembering details overnight, resulting in sloppy billing and late invoices.

Needs & Pain Points

Needs

1. One-tap session-to-invoice on mobile 2. Quick note capture with templates 3. Smart rescheduling that respects work hours

Pain Points

1. Missed charges after back-to-back evening sessions 2. Invoices delayed until late at night 3. Notes lost between commute and home

Psychographics

- Convenience beats customization every time - Hates clutter; loves one-thumb actions - Wants progress without sacrificing evenings - Reliability over ornate feature sets

Channels

1. iPhone — primary 2. WhatsApp — client chat 3. Google Calendar — scheduling 4. Stripe — payments 5. Gmail — confirmations

T

Timezone Tamer Theo

- Independent consultant, 6–12 international clients monthly - Based in Lisbon; serves EU, UK, US - Bills in EUR and USD; uses Wise and Stripe - Tech stack: Google Calendar, Zoom, Slack

Background

Missed early calls due to daylight-saving switches and client-local invites. Confused clients with wrong-currency invoices, causing payment delays and awkward emails.

Needs & Pain Points

Needs

1. Automatic time zone conversion in invites 2. Invoice currency selection per client 3. Localized payment options and VAT fields

Pain Points

1. No-shows from daylight-saving mismatches 2. Payment delays from currency confusion 3. Manual VAT and address corrections

Psychographics

- Clarity is kindness, over-communicate details - Precision with time and money matters - Prefers predictable systems to improvisation - Minimal hassle, maximum transparency

Channels

1. Google Calendar — cross-timezones 2. Zoom — sessions 3. Slack — client comms 4. Stripe — payouts 5. Wise — transfers

W

Workshop Wrangler Wren

- Facilitator/coach, 10–40 attendees per event - Hosts 1–3 workshops monthly; virtual-first - Uses Zoom Meetings, Eventbrite, Mailchimp - Revenue $60k–$120k from group programs

Background

Graduated from ad-hoc Zoom links and spreadsheets for rosters. Lost revenue when attendee counts shifted and manual invoicing lagged.

Needs & Pain Points

Needs

1. Group session scheduling with shared notes 2. Per-attendee billing and attendance tracking 3. Bulk reminders for unpaid participants

Pain Points

1. Unbilled attendees slip through spreadsheets 2. Confusion over who paid or attended 3. Manual roster updates before each session

Psychographics

- Loves group energy and momentum - Operational discipline keeps chaos away - Prefers batch actions over one-offs - Clear communication beats fancy branding

Channels

1. Zoom — group calls 2. Eventbrite — registrations 3. Mailchimp — reminders 4. Google Sheets — rosters 5. LinkedIn Events — promotion

P

Package Planner Priya

- Coach/therapist/consultant with package offerings - 8–20 active package clients concurrently - Uses Stripe, Notion, Google Sheets for tracking - Revenue $80k–$160k; prefers predictable cash flow

Background

Started offering 6- and 12-session packages; credit tracking lived in a messy sheet. Overages and renewals were inconsistent, leaving money on the table.

Needs & Pain Points

Needs

1. Visible session credit balance per client 2. Auto-renew or overage invoicing when exhausted 3. Package-specific note and agenda templates

Pain Points

1. Forgotten renewals after credit exhaustion 2. Miscounted sessions in manual trackers 3. Awkward conversations about expired packages

Psychographics

- Predictability calms and guides planning - Data-backed decisions over instincts - Gentle but firm on boundaries - Clients value clarity and cadence

Channels

1. Stripe — subscriptions 2. Google Sheets — legacy tracker 3. Notion — client hub 4. Gmail — communications 5. Zoom — sessions

N

No-Show Neutralizer Nova

- Early-stage coach, 5–15 sessions weekly - Urban, mobile-first; clients book via social links - Uses Instagram, WhatsApp, Google Calendar, Stripe - Revenue $40k–$90k; high no-show rate history

Background

Started with open booking links; generous rescheduling led to empty hours. Added manual deposits, but reconciling them against sessions became a time sink.

Needs & Pain Points

Needs

1. Prepayment/deposit rules per booking type 2. Automated SMS/email reminders and policies 3. Easy reschedule with deposit carryover

Pain Points

1. Last-minute cancellations without penalties 2. Ghosted invoices after no-shows 3. Manual deposit reconciliation nightmares

Psychographics

- Values commitment and mutual respect - Direct communication, low drama - Prefers guardrails over negotiations - Results matter more than rapport

Channels

1. Instagram — booking traffic 2. WhatsApp — quick messages 3. Google Calendar — scheduling 4. Stripe — deposits 5. Gmail — confirmations

Product Features

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

Intent Detect

Automatically reads incoming emails and DMs to spot scheduling intent, extract preferred times, topics, duration, participants, and meeting mode. Generates a one-click booking draft mapped to your availability so you can confirm in seconds—no back-and-forth or copy-paste.

Requirements

Channel Connectors & Unified Message Ingestion
"As an independent professional using SoloPilot, I want my email and DM accounts connected and messages ingested automatically so that scheduling intents can be detected without manual forwarding or copy-paste."
Description

Build OAuth-based connectors for Gmail/Google Workspace, Outlook/Microsoft 365, and generic IMAP, plus webhook-based connectors for major DM platforms (e.g., Slack/Teams). Ingest inbound messages and metadata in near real-time, normalize into a unified schema, thread conversations, deduplicate, and filter out auto-replies and spam. Detect sender identity and map to existing SoloPilot contacts. Preserve original timestamps, language, and time zone indicators. Enforce least-privilege scopes and encrypted token storage. Expose an internal ingestion event for downstream intent detection and extraction workflows, with idempotency and retry policies for robustness. Provide admin UI to connect accounts, view connection status, and pause/resume ingestion per channel.

Acceptance Criteria
OAuth Email Connectors: Google Workspace and Microsoft 365
- Given an admin opens Admin > Integrations, When they click Connect for Google or Microsoft and complete OAuth consent, Then a new connection is listed with provider in {Google, Microsoft}, channel="Email", and status="Connected" within 10 seconds. - Then the granted scopes are read-only for message content and metadata only (no send/delete/modify scopes), and the exact scopes are displayed in UI and recorded for audit. - Then access/refresh tokens and tenant identifiers are stored encrypted at rest using platform KMS; no plaintext tokens appear in logs or exports. - Given consent is revoked or tokens expire, When the 5-minute health check runs, Then the connection transitions to status="Error" with lastError set, and ingestion is suspended until re-authorized. - Given the connection is Connected, When a new email is delivered to the mailbox, Then an ingestion event is emitted within 10 seconds p95 of provider delivery.
Generic IMAP Connector
- Given an admin enters IMAP host, port, TLS setting, username, and app password (or OAuth2), When Test Connection succeeds and the form is saved, Then a new connection is listed with provider="IMAP", channel="Email", status="Connected". - IMAP sessions must use TLS; credentials are stored encrypted at rest and are never logged in plaintext. - The connector operates in read-only mode; no message modifications or deletions are issued. - New message detection uses IDLE (if supported) or polling at ≤30s interval; upon arrival, an ingestion event is emitted within 30 seconds p95. - Duplicate ingestion is prevented using UIDVALIDITY+UID keys across reconnects; the same message is not stored or emitted twice.
DM Webhook Connectors: Slack and Microsoft Teams
- Given an admin installs the SoloPilot app and completes the workspace/team authorization, When the platform verification challenge is received, Then the app responds within 5 seconds and the connection shows status="Connected" with channel in {Slack, Teams}. - Granted scopes are least-privilege for reading direct messages to the user only; no write or admin scopes are requested; tokens and signing secrets are stored encrypted at rest. - Only direct messages (and threaded replies to those DMs) are ingested; messages from channels are excluded unless explicitly enabled in settings. - For each new DM, a normalized message is persisted and an ingestion event is published within 5 seconds p95 of webhook receipt. - Webhook redeliveries are handled idempotently using event_id (Slack) or messageId/activityId (Teams); no duplicate downstream triggers occur.
Near Real-Time Ingestion, Idempotency, and Retry
- Given a new inbound message arrives on any connected channel, When the connector receives it, Then a normalized record is persisted and an ingestion event is published within 5 seconds p95 (10 seconds p99) of provider delivery. - Each ingestion event includes idempotencyKey composed from provider-specific immutable identifiers and channel; processing the same event multiple times results in a single stored message and one effective downstream trigger. - Transient failures in fetch or publish are retried with exponential backoff up to 5 attempts over 15 minutes; permanent failures are dead-lettered with error class and reason. - The system provides at-least-once delivery guarantees for ingestion events; consumers can de-duplicate using idempotencyKey. - End-to-end latency and retry metrics are exported, with alerts when p95 latency exceeds the SLA for 5 consecutive minutes.
Unified Normalization, Threading, Deduplication, and Metadata Preservation
- For every ingested message, the stored record includes unified fields: provider, channel, messageId, threadId, inReplyTo, from, to, cc, bcc (if available), subject (email), bodyText, bodyHtml (if available), attachments metadata (filename, size, mime), receivedAtOriginal, receivedAtUtc, timezoneOffset, languageCode, and redacted rawHeaders. - Threading links replies to existing conversations using thread identifiers (e.g., RFC822 In-Reply-To/References, Slack thread_ts, Teams replyToId); new roots create a new threadId. - Deduplication prevents multiple records for the same logical message using provider identifiers (e.g., Message-ID, IMAP UIDVALIDITY+UID, Slack ts) with content-hash fallback; duplicate inputs do not create additional records or events. - Original provider timestamp, language, and timezone indicators are preserved without transformation in receivedAtOriginal and related fields, alongside normalized UTC. - Messages classified as auto-replies or spam (e.g., Auto-Submitted, X-Auto-Response-Suppress, known OOO patterns, provider spam flags) are tagged ignored and do not emit downstream events; ignoreReason is stored for review. - All records pass schema validation; optional fields are present as null when unknown.
Sender Identity Resolution and Contact Mapping
- For each ingested message, the system attempts to resolve the sender to an existing SoloPilot contact and sets contactId when matched. - Email: case-insensitive exact match on primary email maps to a single contact; multi-match selects the most recently active contact and records ambiguity=true. - Slack/Teams: resolve via platform userId to known email; if email maps to a contact, link; otherwise store platform handle and mark contactId=null. - If no contact is found, the message remains ingestible with contactId=null; no contacts are auto-created by ingestion. - Mapping decision includes confidence level and method (email_exact, platform_userId_email, none) and is auditable in logs.
Admin UI: Connect, Status, and Pause/Resume per Channel
- Admin > Integrations lists all connections with provider, channel, account label, status in {Connected, Paused, Error, Reconnecting}, lastEventAt, lastErrorAt, and action buttons. - Admin can pause ingestion per connection; while paused, no new ingestion events are published, and a Paused badge appears on the connection. - Admin can resume ingestion; upon resume, the connection returns to status="Connected" and processes any queued/provider-retained messages. - Health checks and recent error summaries are visible; hovering status shows the most recent error and timestamp. - All connect, disconnect, pause, resume, and scope changes are captured in audit logs with actor and timestamp.
Intent Classification with Confidence Thresholds
"As a coach, I want SoloPilot to automatically recognize when a message is trying to set or change a session so that I can respond faster without reading every email in detail."
Description

Implement an NLP classifier that labels inbound messages for scheduling-related intents (e.g., request to book, reschedule, cancel, confirm, availability inquiry). Provide per-intent confidence scores, configurable thresholds, and reasons/explanations (top features/snippets) to support operator trust. Support multilingual detection and handle noisy or partial messages. Surface classification outcomes as events that trigger entity extraction and draft generation. Allow admin-level configuration of which intents should auto-draft vs require manual review. Track precision/recall metrics and latency, targeting sub-500ms inference for typical messages.

Acceptance Criteria
Baseline Intent Classification and Confidence Output
Given an inbound message in a supported language that expresses exactly one scheduling-related intent When the message is processed by the intent classifier Then the response includes a primary_intent in {"request_to_book","reschedule","cancel","confirm","availability_inquiry"} And the response includes a confidences map with a key for each supported intent and values in [0.0,1.0] And primary_intent corresponds to the intent with the highest confidence And the response includes language_code (ISO 639-1), classification_id, and processed_at timestamp
Configurable Per-Intent Confidence Thresholds
Given an admin has configured confidence thresholds per intent with values in [0.0,1.0] And the system persists these thresholds When a message is classified Then the decision.outcome is "above_threshold" if primary_confidence >= threshold[primary_intent], otherwise "below_threshold" And the classification event payload includes primary_confidence and threshold_used And if the outcome is "below_threshold" no automatic drafting action is taken and the item is marked for manual review
Admin Policy: Auto-Draft vs Manual Review
Given an admin policy sets auto_draft=true for {"request_to_book","reschedule"} and auto_draft=false for other intents When a classification outcome is "above_threshold" for an intent with auto_draft=true Then a booking_draft.create event is emitted with the message_id and primary_intent And the draft is generated against the owner's availability without user input When a classification outcome is "above_threshold" for an intent with auto_draft=false Then a review_task is created and visible in the Inbox within 2 seconds When a classification outcome is "below_threshold" Then a review_task is created and no draft is generated
Explanations and Evidence Transparency
Given a message is classified When the classification event is produced Then it includes explanations.top_snippets containing 1 to 3 entries And each entry includes text, start_offset, end_offset, and a non-negative score And each text is an exact substring of the original message corresponding to the offsets And the UI highlights these snippets and displays their scores on the review panel
Multilingual and Noisy/Partial Message Handling
Given held-out test sets for English, Spanish, French, German, Portuguese, and Italian with at least 300 messages per intent per language When evaluated offline Then the macro-average F1 across supported intents is >= 0.82 for English and >= 0.80 for each other supported language And per-intent F1 is >= 0.78 for each supported language Given messages containing up to 10% character-level noise (typos, emoji, repeated punctuation) or code-switching between two supported languages When processed Then the classifier returns a valid primary_intent with macro F1 >= 0.75 and sets language_code to the dominant language Given messages with fewer than 5 tokens or ambiguous intent When processed Then the classifier returns primary_intent="unknown" with primary_confidence <= 0.50 and flags ambiguous=true
Event Emission and Downstream Triggers
Given any classification is completed When the event is emitted Then an intent.classified event is published within 50 ms of model completion with payload: message_id, primary_intent, confidences, language_code, threshold_used, decision.outcome, explanations.top_snippets And an entity.extract.requested event is published within 100 ms of the intent.classified event And if the admin policy and thresholds permit auto-drafting for the primary_intent, a booking_draft.create event is published; otherwise only a review_task is created And all events are deduplicated by classification_id and are idempotent
Latency and Metrics Tracking
Given a production-like load of 10 requests/second and messages <= 1500 characters When measured over 10,000 classifications Then p95 end-to-end classification latency (request received to intent.classified event published) is <= 500 ms and p50 <= 200 ms And per-request inference_duration_ms and end_to_end_duration_ms are logged And daily aggregates of precision, recall, and F1 per intent and language are computed and stored for the last 30 days And a dashboard or API exposes per-intent volumes, p50/p95 latency, and precision/recall time series And an alert is sent to the ops channel if any intent’s 7-day rolling precision drops below 0.80 or p95 latency exceeds 500 ms for 15 consecutive minutes
Entity Extraction for Scheduling Parameters
"As a therapist, I want SoloPilot to pull out the when, how long, who, and how we’ll meet from a client’s message so that I don’t have to retype details into a booking."
Description

Extract structured fields from detected-intent messages, including preferred dates and times (with ranges and alternatives), duration, session topic, participants, meeting mode (in-person, video, phone), location or video platform preference, and time zone. Resolve ambiguities (e.g., “next Tuesday afternoon”), recognize recurrence, and parse attachments or calendar proposals (ICS). Support multilingual and locale-aware parsing. Output a normalized payload with confidence per field and rationale snippets, and flag low-confidence fields for user review. Integrate with SoloPilot contact records to map mentioned participants. Log extraction outcomes for analytics and model improvement.

Acceptance Criteria
Resolution of Relative Time Phrases and Ordered Alternatives
Given a detected scheduling-intent message: "Next Tuesday afternoon works; otherwise Wednesday 3–5pm. 45 min" And the user's account timezone is a valid IANA zone (e.g., America/Los_Angeles) and locale is en-US with daypart mapping afternoon = 12:00–17:00 When the extractor processes the message on a reference date Then it resolves "next Tuesday" to the Tuesday after the reference date and maps "afternoon" to a window 12:00–17:00 local And it parses "Wednesday 3–5pm" to a window 15:00–17:00 local And it sets duration = 45 minutes for both candidates And it returns time_preferences as an ordered list where the first candidate corresponds to "Next Tuesday afternoon" and the second to "Wednesday 3–5pm" And each candidate includes ISO 8601 start/end with timezone, source = "text", a rationale snippet quoting the exact phrase, and confidence (>= 0.85 for the first candidate and >= 0.80 for the second) And if multiple locale interpretations exist causing confidence < 0.80, the affected candidate is included with require_review = true
Participant Identification and Contact Mapping
Given a message mentions participants: sender "Jordan <jordan@client.com>", "Alex Chen <alex@acme.com>", and "Priya (my EA)" And SoloPilot contacts contain records for jordan@client.com and alex@acme.com but not for Priya When the extractor processes the message headers and body Then participants are returned as a deduplicated list including Jordan, Alex, and Priya And Jordan and Alex are mapped to their existing contact_id values by email with confidence >= 0.90 And Priya is included with name = "Priya", contact_id = null, confidence < 0.90, and require_review = true And each participant entry includes source (header or text), rationale snippet (quoted mention), and normalized email if present
Meeting Mode, Location, and Platform Extraction
Given a message text: "Prefer Zoom; if not, phone at +1 (415) 555-0199; or meet at 123 Main St, San Francisco" When the extractor processes the message Then meeting_mode_preferences are returned in the order ["video","phone","in_person"] And platform = "Zoom" is extracted with confidence >= 0.90 and a rationale snippet quoting "Zoom" And phone is normalized to E.164 "+14155550199" with confidence >= 0.90 and a rationale snippet quoting the number And location.address = "123 Main St, San Francisco" is extracted (as a single-line normalized string at minimum) with confidence >= 0.85 and a rationale snippet quoting the address
Recurrence Recognition and Normalization
Given a message: "Let's meet every other Wednesday at 10am for 6 weeks starting next week" And the user's timezone is a valid IANA zone (e.g., Europe/London) When the extractor processes the message on a reference date Then it outputs recurrence.rrule = "FREQ=WEEKLY;INTERVAL=2;COUNT=6;BYDAY=WE" And it resolves start_datetime to the Wednesday of the week after the reference date at 10:00 local in ISO 8601 And it includes rationale snippets for "every other Wednesday", "for 6 weeks", and "starting next week" And it assigns confidence >= 0.85 to the recurrence field
ICS Attachment Parsing and Conflict Resolution
Given an email has an attached ICS proposing Wednesday 15:00 UTC and the body text states "Thursday 3pm works best" When the extractor processes both the attachment and the text Then it returns at least two candidate time options labeled with source = "ics" and source = "text" And each candidate includes ISO 8601 datetimes normalized to the appropriate timezone and a rationale snippet citing its source And the top_candidate is set to the "text" option when preference phrases like "works best" are present And a conflict object is included listing the differing proposals And review_required is set to true if the confidence difference between the top two candidates is < 0.10; otherwise false
Multilingual and Locale-Aware Parsing
Given a message in Spanish: "Podemos vernos el lunes 10/11 a las 16h por Google Meet. Zona horaria: CET." And detected language = "es" and sender locale = es-ES When the extractor processes the message Then the date is interpreted using dd/MM (10 November) with time 16:00 and timezone Europe/Madrid (CET) in ISO 8601 And meeting_mode = "video" and platform = "Google Meet" are extracted with confidence >= 0.90 And each extracted field includes language_code = "es" in its rationale metadata And no MM/DD (en-US) heuristics are applied during parsing
Normalized Payload, Confidence, Rationale, Review Flags, and Analytics Logging
Given a scheduling-intent message is processed When entity extraction completes Then the system returns a normalized payload containing applicable fields: time_preferences or time_windows, duration, topic (if present), participants, meeting_mode, platform or location, timezone, and recurrence, each with ISO-normalized values And every returned field includes: confidence in [0,1], source (e.g., text, ics), and a rationale snippet quoting the evidence span And any field with confidence below the configured threshold (default 0.80) is included with require_review = true and listed in review_fields And the system emits a telemetry event "intent_extract_v1" with request_id, user_id (pseudonymous), model_version, language, fields_extracted count, average_confidence, duration_ms, and success flag And the telemetry event contains no full message body and limits rationale snippets to a maximum of 140 characters
Availability Mapping & Booking Draft Generation
"As a freelancer, I want SoloPilot to turn the parsed details into a ready-to-confirm booking mapped to my real availability so that I can schedule in seconds."
Description

Map extracted preferences to the user’s calendars and SoloPilot scheduling rules (working hours, buffers, service durations, blackout dates, locations, video integrations) to compute the best-fitting slots. Generate a booking draft that includes proposed time(s), participants, location/mode, and service type, with conflicts and constraints respected across connected calendars. Offer ranked slot suggestions, soft-hold options, and automatic time zone alignment. Pre-populate session notes and invoice line items per service configuration. Persist drafts with audit trail and expose them in the SoloPilot dashboard and via notifications for quick confirmation.

Acceptance Criteria
Rule-Compliant Availability Mapping
Given a user has defined working hours, minimum/maximum notice, service durations, buffers, blackout dates, and connected calendars with busy/free transparency And an intent has extracted a service type, preferred windows, duration, participants, and location/mode preferences When the system computes candidate slots Then every returned slot falls within working hours, observes pre/post buffers, meets min/max notice, avoids blackout dates, and matches the service duration And no returned slot overlaps a busy event on any connected calendar And location/mode constraints (e.g., in-person day/location availability) are respected And if no valid slots exist, the system returns a "No slots available" result with a list of violated rule(s) per rejected window
Comprehensive Booking Draft Generation
Given candidate slots have been computed for an intent When a booking draft is generated Then the draft includes: ranked proposed times (default 3, configurable 1–5), participants, service type, duration, requester and host time zones, location/mode, and per-slot soft-hold flags And session notes are pre-populated from the service template with extracted topic/agenda fields inserted And invoice line item(s) are pre-populated from service configuration with correct rate, quantity/duration, taxes/discounts, and computed total And no external invitations or invoices are sent until the draft is explicitly confirmed And all draft fields are editable prior to confirmation
Ranked Suggestions and Soft-Hold Lifecycle
Given slot ranking is required When ranking is performed Then slots are ordered by match to stated preferences (date/time window, duration), earliest acceptable time, and rule fit; ties are broken by least calendar fragmentation And each slot displays its rank position When the user enables soft-hold on proposed slots Then the system creates busy holds on the primary calendar for each selected slot that last 24 hours by default (configurable) And holds prevent double-booking and auto-expire on timeout or manual cancel And on confirmation, the chosen hold converts to a booked event and all other holds are released And while on hold, no external invites are sent
Automatic Time Zone Alignment and DST Safety
Given the requester’s time zone can be derived from headers, message content, or stored profile When presenting proposed times Then times are shown in the requester’s time zone and the host’s local time zone, stored as IANA IDs And DST transitions are correctly applied to start/end times And if the requester’s time zone cannot be determined, the system defaults to the host’s time zone and flags "Time zone unconfirmed" for user review When the draft is confirmed Then the booked event reflects correct times for all participants’ time zones in calendar invites and notifications
Cross-Calendar Conflict Detection and Error Handling
Given multiple connected calendars (e.g., personal, work, resources) with event transparency When computing availability Then events marked Busy on any connected calendar block time; events marked Free do not And all-day Out of Office blocks the entire day And Tentative events block if the user’s preference is set to block tentative; Declined events do not block And if any calendar API is unavailable or times out, the system marks availability as Unknown for affected windows, suppresses proposing slots in those windows, and surfaces a retriable sync error in the draft
Service, Location/Mode, and Video Integration Mapping
Given the selected service defines duration, allowed locations/modes (in-person addresses, phone, video), travel/room constraints, and a default video provider And the intent includes a preferred mode/location when stated When generating slots and the draft Then only slots compatible with the service duration and location/mode constraints are proposed (e.g., travel buffers for in-person, room availability) And the video provider is selected per user default or explicit preference; the meeting link is created on confirmation (not during soft-hold) and populated into the draft And for in-person, the address is included; for phone, call-in instructions from the template are included
Draft Persistence, Audit Trail, Dashboard Visibility, and Notifications
Given a booking draft is created or updated When it is saved Then it is persisted with a unique ID, created/updated timestamps, source message ID(s), extracted fields, applied scheduling rule version(s), ranking rationale, and actor identity And all changes are recorded in an immutable audit log with before/after values And the draft appears in the SoloPilot dashboard Drafts list within 5 seconds and is filterable/searchable by status, service, requester, and date range And notifications (in-app and email) are sent with a deep link to the draft And only one confirmation can succeed per draft; concurrent confirmations are rejected with a clear message And drafts auto-expire and archive after 90 days of inactivity
One-Click Confirm & Smart Reply Composer
"As a consultant, I want to confirm a suggested time and send a polished reply with one click so that I avoid back-and-forth and keep my workflow moving."
Description

Provide a compact UI surface in the SoloPilot inbox and notifications to review extracted details and confirm with one click. Automatically generate a context-aware reply for the original channel (email or DM) with confirmation or proposed times, including an ICS invite or booking link as appropriate. Support editable templates, variable insertion, and tone presets. On confirmation, create the calendar event, send confirmations, update SoloPilot records, and trigger follow-on automations (reminders, notes template, invoice draft). Log all outbound communications and respect channel rate limits and threading.

Acceptance Criteria
One-Click Confirmation from Inbox UI
Given an inbound message with detected scheduling intent and at least one viable time mapped to the user’s availability When the user opens the SoloPilot inbox item or push notification surface and clicks Confirm Then the booking is confirmed without additional forms, required fields (time, duration, participants, mode) are auto-populated, and a success state is shown And the Confirm action is disabled until all required fields are present or resolved And the compact UI displays extracted fields (topic, time, duration, participants, mode) and highlights conflicts before confirmation And p95 time from Confirm click to visible success state is ≤ 5 seconds
Context-Aware Reply with ICS or Booking Link
Given the original channel is email and the user confirms a specific time When the confirmation is sent Then a reply is drafted and sent in the same email thread with correct subject threading headers and includes an ICS invite with accurate start/end time, timezone, attendees, and location/mode And the email body summarizes confirmed details and uses the selected template and tone Given the original channel is a DM that does not support ICS attachments When the confirmation is sent Then a message is posted in the original thread with the confirmed details and an auto-generated booking link or calendar file fallback, formatted per channel conventions
Alternative Time Proposals on Availability Conflict
Given the extracted request time is unavailable or ambiguous When the user clicks Propose Times Then the system suggests 3 alternative slots within the user’s configured working hours and meeting preferences, respecting both parties’ time zones and avoiding conflicts And the reply is composed in the original channel thread listing the proposed slots with one-click selection or a booking link pre-filtered to those slots And if no slots are available in the next 10 business days, the reply explains unavailability and offers the booking link with broader availability
Template Editing, Variable Insertion, and Tone Presets
Given templates with variables are configured When the user selects a template and tone preset in the composer Then the preview renders with resolved variables (e.g., client.first_name, meeting.time, meeting.mode, booking.link) in the correct locale and timezone And unresolved variables are surfaced with inline validation and block send until resolved or removed And the user can edit the message before send, and the final sent content matches the edited preview And the selected tone preset adjusts phrasing per style guide without altering factual details (time, location, links)
Post-Confirmation Event Creation and Automations
Given a confirmation is submitted When back-end processing completes Then a calendar event is created in the connected calendar with correct title, start/end time, timezone, attendees, and conferencing details/mode And attendee confirmation/invite is sent, SoloPilot session record is created/updated, reminders are scheduled, a notes template is attached to the session, and an invoice draft is created with duration and mapped rate And duplicate submissions within 60 seconds are idempotent (exactly one event/invite and one invoice draft) And p95 time from confirmation to event creation is ≤ 10 seconds; downstream artifacts (reminders, notes template, invoice draft) are available within 30 seconds
Channel Threading and Rate Limit Compliance
Given provider-specific threading and rate limits When messages are sent by the composer Then email replies include correct In-Reply-To and References headers; DM replies post in the same thread/conversation And the system enforces provider rate limits with queued sends and exponential backoff; no more than the configured per-minute/hour caps are exceeded And when a rate limit is hit, the user is notified in-app within 5 seconds and the message status shows Pending with next retry time
Outbound Communication Logging and Audit Trail
Given any confirmation or proposal is sent When the outbound communication is dispatched Then an immutable log entry is created capturing timestamp, channel, recipients, subject/snippet, thread/message ID, ICS/link status, and delivery outcome And the log is visible on the session timeline and searchable by recipient, date, and thread ID And each confirmation action results in exactly one logged outbound message and one calendar invite log (or a single message with link for non-ICS channels)
Human Review, Corrections, and Personalization Loop
"As a solo operator, I want to quickly fix any misread details and have SoloPilot learn my preferences so that future drafts are accurate by default."
Description

Enable users to review and correct classifications and extracted fields inline, with changes instantly updating the booking draft. Capture corrections and outcomes as feedback signals to personalize future intent detection and extraction per user, service type, and client. Provide opt-in controls, anonymization, and versioned model configurations. Offer quick actions to teach preferred slots, default durations, and phrasing. Include an evaluation dashboard showing accuracy over time and suggested automations to reduce manual steps.

Acceptance Criteria
Inline Correction Updates Draft in Real Time
Given a detected intent message is open in the review panel When the user edits any extracted field (date/time, duration, participants, meeting mode, topic) Then the booking draft preview updates to reflect the change within 300 ms without a page reload and a single debounced PATCH request (<=300 ms) succeeds with 2xx Given the user triggers Undo on a recent change When Undo is clicked or Cmd/Ctrl+Z is pressed Then the field value reverts to the previous value and the draft preview matches the reverted state Given a network error occurs while saving a change When the PATCH request fails Then an inline error message appears within 1 s, the change is not persisted, and a Retry action is available
Corrections Captured as Feedback Signals for Personalization
Given the user confirms a booking after making corrections When the draft is submitted Then a feedback record is stored with before/after values, field types, message ID, user ID, service type, client ID, and timestamp Given feedback storage is successful When the record is written Then it is retrievable in audit logs within 5 minutes and linked to the draft and message Given repeated similar corrections (>=10 within 14 days) for the same field and service type When the nightly personalization job runs Then a new personalized model/version is queued for training and its pending status is visible in the dashboard within 24 hours Given a correction teaches a preference When the next similar message arrives Then the extracted suggestion reflects the learned preference with a visible confidence score; if confidence < 0.7 the field is highlighted for review
Opt-In Controls, Consent, and Anonymization Enforcement
Given a new user opens Intent Detect settings for the first time When the settings page loads Then personalization/data-sharing is OFF by default with an explicit opt-in toggle and a link to the privacy policy and terms Given a user opts in to personalization When the toggle is turned ON and consent is confirmed Then a consent record is created with user ID, timestamp, policy version, and scope, and appears in the audit log Given anonymization is enabled When storing feedback records Then PII (names, emails, phone numbers) is tokenized and raw message bodies are not stored; a sampled record shows token placeholders instead of PII Given a user revokes consent When the toggle is turned OFF Then new data collection stops immediately, prior records are flagged do-not-train, excluded from subsequent training runs, and this is reflected in the dashboard within 15 minutes
Quick Teach Actions for Preferred Slots, Default Durations, and Phrasing
Given the review panel shows a detected meeting request When the user clicks "Teach preferred slots" Then a selector shows top recurring availability windows and saving creates a rule visible in Settings > Preferences Given a user sets a default duration for a service type When "Teach default duration" is saved as 90 minutes for Service X Then future drafts for Service X default to 90 minutes while remaining editable Given a user defines a confirmation phrasing template When a booking reply is generated Then the reply uses the saved template with correct variable substitution (client name, date, time, mode) and no unresolved placeholders Given a taught slot conflicts with real-time availability When generating a draft Then availability is honored, a non-blocking notice explains the conflict, and the next-best slot is proposed
Evaluation Dashboard: Accuracy Over Time and Suggested Automations
Given the user opens the evaluation dashboard When a date range and filters (user, service, client) are applied Then precision, recall, and F1 per field and overall intent detection accuracy are displayed with counts of evaluated messages Given >=50 evaluated messages exist in the selected period When metrics are computed Then 95% confidence intervals are shown and trend deltas vs. prior period are displayed Given repeated manual edits match a pattern (>=3 occurrences in 7 days) When suggestions are generated (daily) Then suggested automations list includes the pattern with an estimated time saved, and a one-click enable option that creates a disabled-by-default rule for review Given the user clicks on a metric When drilling down Then anonymized samples with predicted vs corrected values and confidence scores are displayed
Versioned Model Configurations and Rollback Controls
Given a new personalized model version is deployed When training completes Then the active model version (e.g., v3) and changelog are visible in settings, and inference requests include the version ID in logs Given accuracy drops by >=5 percentage points week-over-week for any key field When the monitoring job runs nightly Then the system flags degradation, automatically rolls back to the previous stable version, and notifies the user within 1 hour Given a user selects a model option When choosing Global, Personalized, or a specific version in settings Then subsequent extractions use the selected option and the choice persists across sessions Given an audit export is requested When the user clicks Export Configuration Then a JSON file is downloaded including model version, enabled rules, feature flags, training dataset references (hashed/IDs), and current consent state
Privacy, Security, and Compliance Controls
"As a professional handling client information, I want strong privacy controls and transparent handling of my communications so that I can trust SoloPilot with sensitive data."
Description

Implement end-to-end encryption in transit and at rest for message content and tokens, enforce least-privilege OAuth scopes, and isolate tenant data. Provide data retention controls, redact sensitive content in logs, and support regional data residency where available. Include audit logging for access, actions, and model inferences tied to user accounts. Offer consent notices and opt-out for automated processing to meet privacy expectations, and align with applicable regulations (e.g., GDPR). Perform threat modeling, rate limiting, abuse detection, and backup/restore procedures for ingestion pipelines.

Acceptance Criteria
Encryption and Key Management for Intent Detect Pipelines
Given API endpoints for ingesting emails and DMs, When a TLS handshake occurs, Then TLS 1.2+ with modern ciphers is enforced and weak cipher suites are disabled Given service-to-service traffic, When internal calls are made, Then mutual TLS is required with certificate pinning Given message content and tokens stored at rest, When data is persisted, Then AES-256 encryption at rest via managed KMS keys is applied Given KMS-managed keys, When rotation policy is evaluated, Then keys rotate at least every 90 days and rotation events are logged Given an operator or process, When attempting to access plaintext tokens or message content, Then access is denied and secrets remain unreadable without KMS authorization
Access Control: Least-Privilege OAuth and Tenant Isolation
Given a user connects an email or DM provider, When the consent screen is displayed, Then only the minimum read scopes required for intent detection are requested and clearly described Given an issued access token, When calling any provider API beyond granted scopes, Then the call fails with an insufficient scope error Given multi-tenant data stores, When querying with a tenant-scoped principal, Then results only include that tenant’s data due to enforced row-level and object isolation Given cross-tenant access attempts, When a user from Tenant A requests Tenant B resources by ID, Then a 403 or 404 is returned and an audit event is recorded Given infrastructure credentials, When IAM policies are reviewed, Then permissions follow least privilege and are restricted to tenant and environment boundaries
Data Retention, Deletion, and Backup/Restore
Given a tenant retention setting, When set to N days, Then messages, derived metadata, and inferences older than N days are purged within 24 hours Given a user-initiated delete request, When deletion is confirmed, Then data is hard-deleted from primary stores within 24 hours and excluded from all future processing Given deleted data, When backup lifecycle runs, Then the same data is irrecoverably removed from backups within 30 days according to retention policy Given the ingestion pipeline, When a disaster recovery drill is executed, Then data is restorable to the last successful backup with RPO less than or equal to 24 hours and service RTO less than or equal to 4 hours Given restoration completes, When validation runs, Then message counts, indices, and encryption states match pre-incident baselines
Observability: Redaction, Audit Logging, and Inference Traceability
Given application and access logs, When processing messages or tokens, Then tokens, message bodies, and PII are redacted or hashed before logging Given seeded test secrets and PII, When automated log scans run, Then no raw secrets or full PII values are present in any logs Given user or system actions on message data, When actions occur, Then immutable audit events capture actor, tenant, action, resource, timestamp, IP, and outcome Given a model inference, When logged, Then the audit record includes model and version, input reference (hashed), output summary, and a correlation ID to the source message and actor Given audit log storage, When tamper checks run, Then append-only or WORM guarantees and hash chains validate integrity for at least one year
Regional Data Residency Enforcement
Given a tenant selects an EU or US region, When new data is ingested, Then storage and processing occur only in the selected region Given monitoring of data egress, When cross-region transfer is attempted for tenant data, Then the transfer is blocked and an alert is generated Given region-specific resources, When inspecting infrastructure, Then databases, object stores, and log sinks are provisioned in-region with cross-region replication disabled Given regional unavailability, When failover is considered, Then no cross-region failover occurs without explicit tenant approval and a documented exception
Consent, Opt-Out, and Regulatory Alignment
Given Intent Detect is enabled, When first processing a connected inbox or DM, Then a clear consent notice is displayed and explicit opt-in is recorded with timestamp and actor Given a user opts out, When opt-out is saved, Then automated processing stops within five minutes and no further messages are processed Given opt-out, When reviewing history, Then previously processed data remains accessible per retention policy but is not reprocessed Given a data subject access request, When initiated by the tenant admin, Then an export of processed messages and inferences is available within seven days Given a delete request under GDPR, When confirmed, Then the subject’s data is erased across primary systems within 30 days and queued for backup purge
Threat Modeling, Rate Limiting, and Abuse Detection
Given the Intent Detect feature, When conducting threat modeling, Then STRIDE or LINDDUN analysis is documented with critical and high risks mitigated or explicitly accepted with sign-off Given public-facing ingestion endpoints, When traffic exceeds defined limits, Then 429 responses are returned per IP and per tenant with burst and sustained thresholds enforced Given anomalous patterns such as scripted connects, scraping, or spam waves, When detectors trigger, Then offending principals are throttled or blocked and the tenant admin is notified Given abuse safeguards, When an allowlist is configured for false positives, Then allowlisted principals bypass blocks while remaining rate-limited per policy Given abuse events, When reviewed, Then audit entries include detection reason, thresholds breached, action taken, and reviewer notes

Inline Slots

Inserts live, clickable time options directly into your reply, adjusted to the recipient’s timezone and your real-time availability. Clients pick a slot from the message itself to instantly reserve it, cutting friction and preventing double-booking.

Requirements

Live Availability Sync & Conflict Prevention
"As a solo practitioner, I want Inline Slots to reflect my real-time availability and prevent double-booking so that clients can book confidently without scheduling conflicts."
Description

Implement real-time, two-way availability sync that aggregates SoloPilot calendars and connected external calendars (Google, Outlook, iCloud) to surface only bookable times. Enforce service durations, working hours, buffers, minimum lead time, and max daily capacity. When a recipient clicks a slot, create an atomic, short-lived hold to prevent race conditions and double-booking; release the hold automatically on timeout or upon decline. Respect event privacy and busy/free visibility. Provide graceful fallbacks when availability changes between send and click, guiding recipients to the next best time rather than erroring. All bookings created through Inline Slots must update calendars instantly and trigger SoloPilot’s downstream automations (notes templates, invoicing) without manual intervention.

Acceptance Criteria
Real-Time Aggregated Availability Surfaces Only Bookable Slots
Given a user has SoloPilot, Google, Outlook, and iCloud calendars connected with at least one busy event on each When Inline Slots are generated Then only times not overlapped by any busy event are shown Given a private event exists on an external calendar When calculating availability Then the time is treated as busy and event details are not exposed in API/UI payloads Given the user disconnects an external calendar When refreshing availability Then slots are recalculated within 5 seconds and reflect only remaining calendars Given the user creates a new busy event on any connected calendar When availability is refreshed or the page is reloaded Then the new busy time is excluded within 5 seconds
Timezone-Aware Inline Slots Rendering
Given the recipient's timezone is detectable from their browser or email client headers When Inline Slots are rendered Then slot times display in the recipient's timezone and include the timezone abbreviation Given the recipient's timezone cannot be determined When Inline Slots are rendered Then slots default to the sender's timezone and include a visible timezone label Given daylight savings transitions When slots span the transition date Then displayed times and durations are correct in the recipient's timezone Given the sender changes their working timezone When regenerating Inline Slots Then all slot times are recalculated accordingly
Service Rules Enforcement (Duration, Working Hours, Buffers, Lead Time, Capacity)
Given a service duration of 50 minutes and a 10-minute buffer is configured When availability is calculated Then only start times that allow duration plus buffer to fit within working hours are shown Given a minimum lead time of 24 hours When a recipient views Inline Slots Then times earlier than now plus 24 hours are not shown Given a max daily capacity of 5 sessions for a service When 5 bookings exist for the same day Then no further slots are shown for that day for that service Given overlapping services with different buffers When availability is calculated Then buffers are applied per service and prevent overlaps
Atomic Hold on Slot Click with Auto-Release
Given two recipients click the same slot within 2 seconds When the first hold is created Then the second request receives a temporarily held response and is guided to alternate times Given a hold TTL of 90 seconds When a recipient clicks a slot Then the slot is held exclusively for 90 seconds unless confirmed or declined Given a recipient abandons the flow When the hold TTL expires Then the hold is released and the slot returns to availability within 2 seconds Given the recipient declines or navigates away explicitly When the hold is released Then any provisional calendar entries are removed
Graceful Fallback for Changed Availability
Given a slot was visible at send time but is no longer available at click time When the recipient clicks the slot Then the UI shows a non-blocking message and offers the next three closest times Given no alternative slots exist that day When fallback is shown Then the UI offers the next available day with at least one slot Given alternatives are accepted When the recipient selects a new slot Then booking proceeds without requiring the sender to resend Inline Slots
Instant Booking Propagation and Automations Trigger
Given a slot is confirmed When booking is completed Then a SoloPilot calendar event is created and all linked external calendars are updated within 5 seconds Given booking creation When the event is saved Then SoloPilot downstream automations fire: the notes template is attached to the session and an invoice draft is created with the correct rate and time Given an external calendar write failure When booking is completed Then the SoloPilot event and automations still complete, and the system retries external sync up to 3 times and notifies the user
Privacy and Busy/Free Compliance
Given external calendars have private events When availability is computed Then only busy/free status is fetched and stored; event titles, attendees, and descriptions are neither logged nor exposed to recipients Given API logs and audit trails When inspecting records for availability computations Then no private event metadata beyond start/end and busy state is present Given data processing agreements When the user requests a GDPR export Then availability-derived data excludes third-party event details
Timezone Detection & Locale Rendering
"As a recipient in a different region, I want the proposed times shown in my local timezone so that I don’t need to convert or risk choosing the wrong slot."
Description

Detect and render slot times in the recipient’s timezone automatically using reliable signals (recipient profile, prior bookings, message headers) while allowing manual override. Format times and dates per locale (12/24-hour, weekday names, DST rules) and clearly indicate the timezone displayed. Support multi-recipient messages by defaulting to the first recipient and including a lightweight selector on the confirmation page for others. Handle daylight saving transitions and edge cases by suppressing ambiguous or non-existent times. Cache timezone context per contact in SoloPilot to keep future communications consistent.

Acceptance Criteria
Primary Timezone Detection from Recipient Signals
Given a recipient has a profile timezone set in SoloPilot When the user composes an Inline Slots message Then all inserted slot times render in the recipient’s profile timezone And the timezone indicator displays the IANA ID and current UTC offset Given no profile timezone exists but the recipient has a prior confirmed booking within the last 12 months When composing Inline Slots Then the booking’s timezone is used for rendering Given neither profile nor booking timezone exists but message headers contain a parsable timezone or offset When composing Inline Slots Then the header-derived timezone is used And the source of detection (Profile, Booking, Header, Fallback) is logged for observability Given multiple signals conflict When determining timezone Then apply precedence: Profile > Recent Booking > Message Headers > Workspace Default Given no signals are available When composing Inline Slots Then the workspace default timezone is used
Locale-Aware Rendering and Clear Timezone Indicator
Given a recipient locale is known (e.g., en-US, en-GB, fr-FR) When slots are rendered Then time format follows CLDR rules for that locale (12/24-hour, date order) And weekday names render in the locale language And DST rules are applied for the timezone at the slot’s instant And a visible timezone indicator appears adjacent to the slots Given the recipient locale is unknown When slots are rendered Then default to a 24-hour ISO-like format with weekday (e.g., 2025-10-07 Tue 15:30) And include the IANA timezone and current UTC offset in the indicator Given the sender and recipient share the same timezone When rendering slots Then still display the timezone indicator to avoid ambiguity
Manual Timezone Override in Composer
Given the composer shows a timezone selector defaulted to the detected timezone When the user selects a different timezone before sending Then all inline slot times update immediately to the selected timezone And the selected timezone persists with the draft Given the user checks "Set for contact" When the message is sent Then the contact’s cached timezone updates to the selected timezone Given the user does not check "Set for contact" When the message is sent Then the override applies only to that message and does not update the cache Given a manual override is applied When availability is recalculated Then availability constraints and double-booking protection remain unchanged
Multi-Recipient Defaulting and Confirmation Page Selector
Given a message is addressed to multiple recipients When composing Inline Slots Then default the rendering timezone to the first To: recipient with a contact record And display which recipient’s timezone is being used next to the slots Given any recipient clicks a slot and lands on the confirmation page When multiple recipients were on the original message Then show a lightweight selector to choose the intended attendee And display their timezone beside their name And changing the selected attendee updates displayed times to that attendee’s timezone only And the booked slot and calendar event details remain unchanged; only the display timezone updates Given the selected attendee has no cached timezone When confirming the booking Then allow manual timezone selection and cache it upon successful booking
DST Transitions and Ambiguous/Non-Existent Time Suppression
Given a recipient timezone has a spring-forward transition creating non-existent local times When generating slots for that period Then suppress any non-existent local times from the options Given a recipient timezone has a fall-back transition creating ambiguous local times When generating slots for that period Then suppress ambiguous times from the options And ensure adjacent valid times render with the correct offset after the transition Given a day affected by DST When rendering the slots list Then show an unobtrusive note if the number of visible slots is reduced or increased due to DST Given automated tests run When validating timezone behavior Then include cases for America/New_York, Europe/Berlin, and Asia/Amman for the next 5 years
Per-Contact Timezone Caching and Refresh Logic
Given a timezone is detected or manually set for a contact When the message is sent or a booking is confirmed Then store the contact’s timezone as an IANA ID and a last-validated timestamp Given a cached timezone exists and is less than 6 months old When composing Inline Slots for that contact Then reuse the cached timezone without re-detection Given a new confirmed booking occurs in a different timezone for the same contact When composing the next message to that contact Then prompt the user to update the cached timezone; if accepted, update the cache and use the new timezone Given contact data governance rules When a contact is deleted Then remove the associated timezone cache entries And expose API endpoints to read/write the contact timezone with audit trail entries on change
Inline Slot Generation & One‑Click Insertion
"As a busy consultant replying to a client, I want to drop in live time options with one click so that I can schedule without leaving my email."
Description

Enable users to insert live, clickable time options directly into replies from SoloPilot’s composer and popular email clients (Gmail and Outlook web/desktop) via extension or copy-to-clipboard snippet. Allow selection of service type, duration, location (e.g., video link, phone, in-person), number of options, and earliest/latest windows. Render slots as accessible, tappable chips that resolve through secure magic links, with graceful plaintext fallbacks for clients that strip HTML. Ensure snippets remain valid after send by resolving availability at click time and updating styling to reflect expired or taken slots. Provide a quick preview before insertion and remember last-used settings per user.

Acceptance Criteria
Cross-Platform One‑Click Insertion From Composer
Given the user is composing in SoloPilot, When they click "Inline Slots", Then the slot picker opens within 300 ms. Given Gmail Web, Outlook Web, or Outlook Desktop with the SoloPilot extension, When the user invokes "Insert Inline Slots", Then the slots are inserted at the cursor position, preserve surrounding formatting, and the message remains editable. Given no extension is available, When the user uses "Copy snippet" and pastes into the email, Then the pasted content renders functional clickable chips in HTML clients and includes plaintext fallback markers. Given insertion completes, Then the block contains service name, duration, location label, recipient timezone label, and the configured number of options. Given the user presses Ctrl/Cmd+Z, Then the entire inserted block is undone in a single step.
Slot Generation With Configurable Parameters
Given the user selects service, duration, location, number of options (1–10), earliest and latest windows, When Generate is clicked, Then the generated slots fall within the window, match the duration, respect busy events across all connected calendars, and equal the requested count unless fewer are available. Given fewer contiguous slots are available than requested, Then the system returns all available matches and labels the preview accordingly. Given a location is selected (video, phone, in-person), Then the slot carries the correct meeting link or instructions into the booking flow. Given the earliest window is later than the latest window or no availability exists, Then the UI displays "No available times" and disables Insert.
Recipient Timezone Auto-Adjustment
Given a known recipient, When the picker opens, Then the recipient timezone is auto-detected and displayed; if detection fails, it defaults to the sender's timezone with a visible override. Given the preview, Then times display in the recipient's timezone with an explicit abbreviation and UTC offset. Given multiple recipients with differing timezones, Then a note indicates which timezone is applied and allows manual override before insertion. Given a DST boundary within the selected window, Then times reflect the correct offsets.
Accessible Chips and Plaintext Fallback
Given an HTML-capable client, Then each slot renders as a button with role="button", an accessible name "Book [Day] [Time] [TZ]", is reachable via keyboard, shows a visible focus indicator, has minimum 44x44 px tap area, and meets WCAG 2.1 AA contrast (>= 4.5:1). Given HTML is stripped, Then the email shows a plaintext list of options in the format "1) Day, Date, Start–End (TZ) – [Book link]" plus a fallback booking link. Given a screen reader, When the user navigates to a chip, Then it announces status (Available/Taken/Expired) and activation instructions.
Magic Link Resolution and State Styling
Given a recipient clicks a slot, When the server validates availability, Then, if available, booking proceeds with service, duration, location, and recipient email pre-filled and confirmation completes within 2 steps. Given a clicked slot is taken or expired, Then the landing shows "No longer available" and offers at least three nearest alternative times consistent with the original settings. Given an email client that loads remote content, Then chips fetch current state and visually show Taken/Expired without breaking layout; if remote content is blocked, the click-through page reflects state accurately. Given magic links, Then tokens are signed, single-use, scoped to user/service/slot, and expire no later than 30 days after send or immediately after slot end, whichever comes first.
Preview and Sticky Settings
Given the picker is configured, When Preview is opened, Then it shows exactly how chips and plaintext fallback will render, including timezone labels, and Insert is disabled until preview is generated. Given Insert is clicked, Then the inserted content matches the preview byte-for-byte. Given the user returns to Inline Slots later, Then the last-used settings (service, duration, location, options count, earliest/latest windows, timezone preference) are pre-populated and synced to the user's account so they persist across devices within 5 minutes.
Double-Booking Prevention and Concurrency
Given two recipients attempt to book the same slot at roughly the same time, Then only the first confirmed booking succeeds; subsequent attempts surface "No longer available" with alternatives; no duplicate calendar entries are created. Given a calendar event is added to the user's calendar between email send and slot click, Then the system marks the slot unavailable at click time and prevents booking. Given booking is confirmed, Then the event is created on the correct calendar with service, duration, location, and attendee details, and the slot chips subsequently display Taken in clients that load remote content.
Instant Booking & Confirmation Flow
"As a client, I want my chosen time to be immediately confirmed with a calendar invite so that I know the meeting is set without extra back-and-forth."
Description

On slot click, route the recipient to a minimal confirmation step or auto-confirm based on sender’s settings. Validate availability, apply buffers, and finalize the reservation in under two seconds. Optionally collect lightweight intake questions and display price or payment instructions when applicable. Send confirmation emails to both parties, generate calendar invites (ICS) and conferencing links, and include reschedule/cancel links respecting policy windows. Automatically create the session record in SoloPilot, attach the relevant notes template, and prime invoicing automation as configured.

Acceptance Criteria
Auto-Confirm on Slot Click
Given the recipient clicks a live inline slot and the sender’s booking mode is Auto-confirm When the click is received Then the system validates the slot against real-time availability and applies configured pre/post buffers And the reservation is finalized within 2 seconds end-to-end (95th percentile) And the recipient is routed to a success page showing start/end time in the recipient’s timezone, service name, and conferencing link And the page displays price or payment instructions when configured And the slot is immediately removed from availability to prevent any subsequent booking of the same time
Minimal Confirmation With Intake
Given the recipient clicks a live inline slot and the sender’s booking mode requires a minimal confirmation step with optional intake questions When the confirmation page loads Then the page is prefilled with the selected time in the recipient’s timezone and required/optional intake fields per service configuration (max 5 fields) And field validations (required, format) are enforced client- and server-side When the recipient submits the form Then the system re-validates availability and buffers and finalizes the reservation within 2 seconds end-to-end (95th percentile) And a confirmation screen shows time, conferencing link, and price/payment instructions when applicable
Availability Validation and Alternatives
Given a slot click is received When the selected slot is no longer available or violates buffer/availability rules Then the booking is blocked and no reservation is created And the recipient is shown a clear message that the slot is no longer available And at least 3 next available alternative slots are displayed in the recipient’s timezone based on current real-time availability And the failure response is returned within 2 seconds end-to-end (95th percentile) And no confirmation emails or calendar invites are sent
Concurrent Clicks and Double-Booking Prevention
Given two or more recipients click the same slot within a short interval When the system processes these requests Then at most one reservation is created for that slot And all subsequent requests for the same slot receive an "unavailable" message with alternative slots And no double-booked events appear in any connected calendars And the winner’s reservation is finalized within 2 seconds end-to-end (95th percentile)
Confirmation Emails, ICS, and Conferencing
Given a reservation is finalized When notifications are dispatched Then both parties receive confirmation emails within 60 seconds And each email includes an ICS attachment with correct start/end time, timezone, organizer, attendee, UID, and conferencing link in the Location or conferencing field And the conferencing link is generated per service settings and is accessible by both parties And emails include reschedule and cancel links that encode the booking and respect policy windows
Session Record, Notes Template, and Invoicing Automation
Given a reservation is finalized When post-booking actions run Then a SoloPilot session record is created with service, client, start/end time, timezone, and pricing (when configured) And the relevant notes template for the service is attached to the session And invoicing automation is primed per configuration (e.g., draft invoice created or post-session invoice rule armed) with pre-populated line items and due-date rules And the recipient’s confirmation screen shows price or payment instructions as configured
Reschedule/Cancel Links Respect Policy Windows
Given a booking exists with reschedule/cancel policies (e.g., 24-hour cutoff) When the recipient clicks reschedule or cancel from the email or confirmation page Then within the allowed window, the action proceeds: reschedule shows current availability (recipient’s timezone), and upon confirmation updates the event, sends updated emails/ICS, and regenerates conferencing details if required And outside the allowed window, the action is blocked with a clear policy message and no changes are applied And any successful change updates availability in real time and preserves invoice/session linkages
Templates, Rules & Personalization Controls
"As a therapist managing different session types, I want templates that auto-apply the right durations and buffers so that I don’t have to reconfigure slots for every message."
Description

Provide a configuration UI to define reusable Inline Slot templates with defaults for service, duration, location, buffers, lead/cutoff times, and number of options. Support personalization tokens (e.g., recipient first name, service name) and conditional rules (new vs. existing client, paid vs. free consult) to tailor availability windows and messaging. Allow per-channel behavior (email vs. chat), limits on how often the same contact is offered slots, and a fallback to a booking page when no suitable slots exist. Expose team-wide presets for consistency while allowing user-level overrides.

Acceptance Criteria
Create and Apply Inline Slot Template Defaults
- Given I am in Inline Slots settings, When I create a template specifying service, duration (5–480 min), location, pre/post buffers (0–240 min), lead time (>=0 min), cutoff time (>=0 min), and number of options (1–10), Then invalid fields are flagged with inline errors and Save is disabled until all validations pass. - Given a valid template, When I save it, Then it appears in the template list with name and last-updated timestamp and is available in the composer. - Given I am composing a message and select this template, When I insert Inline Slots, Then the inserted options reflect the template defaults, pull from my real-time availability, and are converted to the recipient’s timezone. - Given I select the template in the composer, When I preview before insertion, Then I see the exact options and message that will be inserted.
Personalization Tokens Render with Fallbacks
- Given a template uses tokens {{recipient.first_name}} and {{service.name}} in subject/body, When inserting for a contact with those values, Then the tokens render with the correct values and casing. - Given a token’s data is missing for a contact, When inserting, Then the token renders with the template-defined fallback; if no fallback exists, it renders as an empty string with no token braces. - Given a template contains an unknown or malformed token, When attempting to save, Then the system blocks save and shows a validation error identifying the token. - Given preview is opened for a selected contact, When viewing the message, Then all tokens display their resolved values as they will appear to the recipient.
Conditional Rules by Client and Consult Type
- Given rules exist targeting “new client” vs “existing client” and “paid consult” vs “free consult,” When the contact matches a rule, Then the specified availability window and message variant are applied to the inserted Inline Slots. - Given multiple rules match, When inserting, Then rules are evaluated in priority order (top-to-bottom) and the first match is applied; the applied rule is indicated in the composer. - Given no rules match or the contact classification is unknown, When inserting, Then the template’s default rule is applied. - Given the contact’s classification changes, When reinserting, Then the output reflects the newly applicable rule.
Per-Channel Behavior: Email vs Chat
- Given per-channel settings are configured, When inserting via the email composer, Then Inline Slots render as an HTML block with up to the email-specific maximum number of options and include the configured subject/body text. - Given per-channel settings are configured, When inserting via a chat composer, Then Inline Slots render as compact, plain-text options or supported quick-reply buttons with up to the chat-specific maximum number of options. - Given a channel is not fully supported for rich slots, When inserting, Then the system automatically uses the channel’s fallback format without error. - Given different per-channel limits, When changing the channel, Then the number of options auto-adjusts to the channel’s limit and remains within 1–10.
Offer Frequency Limits to the Same Contact
- Given a per-template limit of N days and a global limit of M days, When attempting to insert Inline Slots for the same contact within the shorter of N or M since the last offer was sent, Then the system prevents insertion and displays a warning with the remaining wait time. - Given I have override permission, When I confirm an override, Then the system inserts the slots and records the override event with timestamp and user. - Given the contact is outside the limit window, When inserting, Then no warning is shown and slots insert normally.
Fallback to Booking Page When No Suitable Slots Exist
- Given the template’s rules, buffers, lead/cutoff times, and real-time availability yield fewer than the required number of options, When inserting, Then no time options are inserted and the configured fallback message with a booking page link is inserted instead. - Given a service, duration, and location are defined in the template, When fallback occurs, Then the booking link preselects those values and opens to the earliest available date. - Given fallback occurs, When inserting, Then the inserted content contains only the fallback message and link (no empty slot UI).
Team Presets with User-Level Overrides
- Given a team admin has published a preset template, When a user selects it, Then the preset is available for use and can be set as the user’s default. - Given the preset has locked fields (e.g., service, duration), When the user attempts to edit those fields, Then they are read-only and retain the team-defined values; unlocked fields can be edited and saved as the user’s override. - Given the admin updates a locked field in the team preset, When the user next inserts slots using that preset or an override based on it, Then the locked field reflects the updated team value. - Given a user has created overrides, When they choose Reset to team preset, Then all unlocked fields revert to team values and the override label is removed.
Tracking, Security & Compliance
"As a practice owner, I want insight into which slot inserts convert and assurance that links are secure so that I can optimize scheduling and protect client data."
Description

Track impressions, clicks, and bookings for each Inline Slots insertion to report conversion rates by channel, template, and service. Maintain an audit trail of who clicked which link and when, with slot status transitions for support and compliance. Secure slot links with signed, expiring tokens, single-use redemption, and rate limiting to deter enumeration and abuse; never expose PII in URLs. Provide link invalidation on demand and automatic expiration after booking windows close. Ensure accessibility (WCAG 2.1 AA) for slot elements and confirmation pages, and support localization of strings. Comply with GDPR/CCPA, honoring contact consent and data retention policies.

Acceptance Criteria
Conversion Tracking by Channel, Template, and Service
Given an Inline Slots insertion with identifiers insertion_id, template_id, service_id, and channel When the recipient opens the message and remote content loads Then record exactly one impression event per recipient and insertion within a 24-hour window with timestamp (UTC), channel, template_id, service_id, recipient_ref (pseudonymous), and timezone used Given the recipient clicks any slot link in the insertion When the click is received Then record a click event with insertion_id, slot_id, token_id, timestamp (UTC), and user_agent, deduplicated per recipient per slot within 5 minutes Given the recipient completes a booking from the insertion When the booking is confirmed Then record a booking event linked to the prior click and slot_id with timestamp (UTC) and service_id, and associate invoice value if applicable Given events have been collected for an insertion When metrics are requested via API or dashboard Then conversion rates (impressions→clicks→bookings) are computed and match underlying event counts within ±1% and are filterable by channel, template_id, and service_id Given a message is forwarded or viewed multiple times by the same recipient When multiple impressions occur Then de-duplication rules prevent inflated counts per recipient per insertion as defined above Given tracking is disabled by policy or consent When an impression or click occurs Then only aggregate non-identifying counts are incremented; no recipient_ref is stored
Immutable Audit Trail of Clicks and Slot Status Transitions
Given any slot link is requested When the request is processed Then append an audit record with token_id (if present), insertion_id, slot_id (if present), actor_type (recipient/system/admin), actor_ref (pseudonymous), ip (hashed or truncated per policy), user_agent, event_type (impression|click|redeem|invalid|expired), and timestamp (UTC) Given a slot status changes (Available→Held→Booked→Cancelled→Expired) When the transition occurs Then append an audit record capturing previous_status, new_status, cause (user/system/admin), and booking_ref (if applicable) Given audit records are stored When an update or delete is attempted on an existing record Then the operation is blocked (HTTP 403) and the attempt is logged as a separate audit event; records are append-only Given a support agent queries the audit trail by insertion_id or token_id When up to 10,000 related events exist Then results return in chronological order within 2 seconds and include a stable, paginated cursor API Given an audit export is requested for a contact When the export is generated Then only records within the configured retention window are included and fields are redacted per policy
Signed, Expiring, Single-Use Links with Abuse Mitigations
Given an Inline Slots URL is generated When the link is created Then it contains a signed token with at least 128 bits of entropy, scoped to insertion_id and (optionally) slot_id, includes an expiration not exceeding the configured maximum (default 7 days), and contains no PII in path or query Given HTTPS enforcement is enabled When a slot URL is requested over HTTP Then redirect using 301 to HTTPS without exposing the token in referrers or logs; server logs redact tokens Given a token is redeemed to book a slot When redemption succeeds Then mark the token as redeemed (single-use) and any subsequent redemption attempts respond 410 Gone with a localized safe message and no state change Given invalid or missing tokens exceed the configured threshold (default 10/minute, burst 20) from a single IP When further requests arrive within the window Then respond 429 Too Many Requests for 15 minutes and emit a security alert with counts and IP metadata Given enumeration patterns are detected (e.g., ≥100 unique invalid tokens in 5 minutes across IPs) When the threshold is met Then emit a high-severity alert and activate WAF blocking for the affected route
On-Demand Link Invalidation and Automatic Expiration
Given a user revokes a specific token or all tokens for an insertion via dashboard or API When revocation is confirmed Then within 60 seconds all subsequent requests with those tokens respond 410 Gone with a localized guidance message; revocation is recorded in the audit trail Given the booking window for a service or slot has closed (slot start minus configured buffer) When a token is presented Then the token is treated as expired and returns 410 Gone without changing slot status Given a token was revoked or expired When a replacement link is generated for the same recipient Then a new token is issued and the old token cannot be reactivated Given a booking was confirmed via a token When the originating token is later revoked Then the booking remains active; cancellation requires explicit action, which is captured in the audit trail
WCAG 2.1 AA Accessibility for Slots and Confirmation
Given the slot picker and confirmation pages are rendered When navigated using only a keyboard Then all interactive elements are reachable in logical order, have visible focus indicators, and no keyboard traps exist Given a screen reader user navigates the UI When slots and actions are announced Then each slot exposes an accessible name that includes date, time, timezone, availability, and selection state; errors and confirmations are announced via appropriate aria-live regions Given the UI is viewed at 200% zoom and/or high-contrast mode When layout adjusts Then content reflows without loss of information or functionality, controls remain operable, and color contrast meets or exceeds 4.5:1 for text and 3:1 for UI components Given the page language and direction are set When a locale is applied Then html lang and dir attributes reflect the locale, and RTL locales have correct reading and focus order Given icons or non-text elements convey meaning When rendered Then they have text alternatives or aria-labels that convey equivalent information
Localization of Slot UI and Confirmation
Given the recipient’s preferred locale is known or auto-detected When rendering slot UI and confirmation pages Then all user-facing strings appear in that locale with fallback to English for missing translations; no hardcoded strings remain Given date and time values are displayed When formatted for the recipient Then they follow locale conventions (12/24h, month/day order), include explicit timezone label/offset, and provide a control to change timezone Given an RTL locale (e.g., ar, he) is selected When the UI renders Then layout, alignment, and icons mirror correctly, and caret/focus behavior is appropriate for RTL Given pseudolocalization is enabled in staging When navigating the widget Then no string truncates or overflows and translation coverage is 100% Given ICU MessageFormat or equivalent is used for templates When variables (e.g., service name) are inserted Then grammar and pluralization are correct for the target locale
GDPR/CCPA Consent, Retention, and Data Subject Requests
Given a recipient is in a jurisdiction requiring consent and consent has not been granted When an impression or click occurs Then either no event is stored or only aggregate non-identifying counts are updated; no persistent recipient_ref is stored; booking remains functional Given a contact withdraws consent When processing future impressions or clicks Then identifying tracking for that contact is disabled within 24 hours and no new identifying events are stored thereafter Given a data deletion request is submitted and verified When processing the request Then personal identifiers in tracking and audit records for that contact are deleted or irreversibly anonymized within the configured retention period (max 30 days), verified by a post-deletion export Given a data export (DSAR) is requested When generating the export Then a machine-readable package of the contact’s related audit and booking interactions is produced within 30 days, excluding secrets and proprietary security data Given URLs are generated for recipients When links are created Then no PII appears in URL path, query, or fragments; tokens do not encode PII; server/application logs redact tokens and PII Given data retention policies are configured When nightly maintenance runs Then events older than the configured period are purged or aggregated, and purge operations are logged in the audit trail

Reply-to-Book

Lets clients confirm by simply replying with natural language (e.g., “Tuesday 3pm works”) or tapping a quick action in DMs. SoloPilot interprets the response, books the slot, and sends confirmations plus intake forms—asking a smart follow-up if details are unclear.

Requirements

Natural Language Parsing & Intent Resolution
"As a client, I want to reply with natural language like “Tuesday 3pm works” so that SoloPilot books the right session without making me fill out a form."
Description

Implement a parser that interprets free‑text replies to determine booking-related intents (confirm, propose time, reschedule, cancel) and extract entities such as date/time, timezone, service, duration, location, and participant. Support relative expressions (e.g., “next Tuesday at 3”, “tomorrow afternoon”), multilingual phrases, and common ambiguities with a confidence score. When confidence is below threshold or required details are missing, automatically ask targeted follow-up questions to disambiguate. Resolve services and durations against the provider’s SoloPilot catalog and client context. Integrate with SoloPilot’s messaging layer and persist a structured intent object for downstream scheduling and automation.

Acceptance Criteria
Confirm Booking via Natural Language Reply
Given a conversation with a pending proposed slot X for client C And client replies 'Tuesday 3pm works' matching slot X When the parser processes the reply Then intent = confirm And extracted datetime equals slot X in ISO 8601 And confidence >= 0.90 And serviceId and duration are inherited from the proposal context And a structured intent object is persisted with fields [intent, datetime, timezone, serviceId, duration, confidence, sourceMessageId, clientId, providerId] And a confirmation message is sent in the same thread
Propose New Time with Relative Date Expression
Given a client C with profile timezone Z and no explicit timezone in the message And client replies 'next Tuesday at 3' When the parser processes the reply at message timestamp T Then intent = propose_time And extracted datetime equals the calendar date of the next Tuesday at 15:00 in timezone Z relative to T And confidence >= 0.85 And entities [datetime, timezone] are present And a structured intent object is persisted and linked to the source message
Reschedule Request with Partial Details and Follow-up Disambiguation
Given an existing scheduled appointment A for client C in the thread And client replies 'Can we move to tomorrow?' When the parser processes the reply Then intent = reschedule And required entity time is missing And a targeted follow-up question requesting a specific time is sent within 2 seconds p95 When client replies 'any time after 2pm EST' Then timezone is resolved to America/New_York and time range start = 14:00 And overall confidence >= 0.85 after follow-up And the structured intent object is updated to include resolved entities and cross-references both messages
Cancel Intent Detection with Multilingual Phrases
Given a client C and at least one upcoming appointment And client replies in Spanish 'Necesito cancelar mi cita de mañana' When the parser processes the reply Then intent = cancel And the target appointment is resolved to tomorrow relative to client's timezone And confidence >= 0.90 And if multiple candidate appointments exist, a disambiguation message listing candidates is sent; otherwise a cancellation confirmation is sent And a structured intent object is persisted including the target appointment id
Service and Duration Resolution Against Provider Catalog
Given a provider catalog with services [Initial Consultation 60m, Follow-up 45m] And client replies 'Let's do a follow-up next Tue at 3' When the parser processes the reply Then intent includes service resolution And serviceId maps to 'Follow-up' from the catalog and duration = 45 minutes And mapping confidence >= 0.85 for auto-selection And if multiple services match above threshold, a follow-up is sent with up to 3 options sorted by confidence And the structured intent object contains serviceId and duration
Timezone and Location Extraction with Ambiguity Handling
Given client profile timezone America/Los_Angeles and provider locations [Office Downtown, Telehealth] And client replies '3pm PST at your office' When the parser processes the reply Then timezone is set from explicit 'PST' and overrides profile timezone And location resolves to 'Office Downtown' And if 'office' maps to multiple locations, a follow-up asks the client to choose And datetime is normalized to ISO 8601 and includes timezone offset And confidence >= 0.85
Structured Intent Persistence and Messaging Integration
Given any resolved or follow-up-triggered intent When parsing and resolution complete Then a structured intent object is persisted with fields [id, clientId, providerId, intentType, entities{datetime, timezone, serviceId, duration, location, participants}, confidence, messageIds, createdAt] And persistence is idempotent for duplicate sourceMessageId And persistence latency is <= 300 ms p95 from parse-complete to commit And confirmation/follow-up messages are sent via the messaging layer within 1 s p95 in the same thread
Real-Time Availability & Slot Reservation
"As a provider, I want the system to validate and hold requested times automatically so that I avoid conflicts and double bookings."
Description

Check provider availability in real time across connected calendars (SoloPilot schedule, Google, Outlook/Exchange) applying service rules (duration, buffers, locations), working hours, and double-booking policies. Convert client-proposed times to the provider’s timezone, handle daylight savings, and detect conflicts. If the requested time is unavailable, compute and propose the nearest alternatives that satisfy constraints. Support soft holds during clarification/confirmation windows to reduce race conditions, with automatic release on timeout or decline.

Acceptance Criteria
Real-Time Availability Across Connected Calendars
- Given the provider has SoloPilot, Google, and Outlook calendars connected and a service with duration/buffer/location/working hours configured - When a client proposes a specific date/time and service via Reply-to-Book - Then the system queries all connected calendars and compiles availability in real time, returning a preliminary decision within 2 seconds - And the candidate slot is validated against service duration, pre/post buffers, provider working hours, location constraints, and double-booking policy - And the slot is marked unavailable if any constraint fails; otherwise it is eligible for hold/booking
Client Proposal Converted to Provider Timezone with DST Handling
- Given the client’s timezone is known (profile or message metadata) and the provider’s timezone is set in SoloPilot - When the client replies with a natural-language time (e.g., “Tue 3pm”) that falls on or near a daylight saving transition - Then the time is interpreted in the client’s timezone and converted to the provider’s timezone accurately - And if the local time is ambiguous (DST fallback), the system asks a clarifying follow-up - And if the local time is non-existent (DST spring-forward), the system rejects the exact time and proposes the nearest valid times - And all confirmations display times in the client’s timezone, with the provider’s timezone noted
Soft Hold During Clarification to Prevent Race Conditions
- Given a candidate slot is eligible but a clarification or explicit confirmation is pending - When a soft hold is placed - Then a tentative/hold event is created in SoloPilot and mirrored to external calendars with a unique hold ID - And concurrent booking or hold attempts on the same slot are blocked with a “temporarily held” response - And the hold duration equals the configured window (default 10 minutes) and is visible to the provider - And the hold is automatically released on timeout or explicit decline and all tentative events are removed within 10 seconds - And all hold/acquire/release actions are recorded in the audit log
Nearest Alternative Slots Computation
- Given a requested time is unavailable after applying all constraints - When computing alternatives - Then at least 3 nearest valid slots within the next 7 days are generated (or all available if fewer than 3 exist) - And options are sorted by temporal proximity to the requested time and do not include slots that violate buffers, working hours, locations, or double-booking policy - And no duplicate or overlapping options are presented - And each option is returned with a booking-ready quick action
Double-Booking Policy Enforcement by Service
- Given a service has a configured double-booking policy of NoOverlap or AllowOverlapUpToN - When evaluating availability for that service - Then for NoOverlap any overlap (including buffers) with existing events marks the slot unavailable - And for AllowOverlapUpToN up to N concurrent bookings of that service are permitted; the (N+1)th overlapping request is marked unavailable - And policy changes take effect immediately for new holds and bookings
External Calendar Sync Integrity and Atomic Booking
- Given Google and/or Outlook calendars are connected - When evaluating, holding, or booking a slot - Then if the last remote sync is older than 60 seconds, a refresh is performed before finalizing the decision - And holds/bookings are written atomically: either all calendar writes succeed or the operation is rolled back and the user is notified - And on external API failure or rate limiting, the system retries up to 3 times with exponential backoff and surfaces a clear, actionable error - And a final conflict check is performed after successful writes; if a conflict is detected, the operation is rolled back and alternatives are proposed
Channel Connectors & Quick-Action CTAs
"As a client, I want tappable confirm buttons in my messages so that I can book instantly without typing."
Description

Provide inbound/outbound connectors for common channels (SMS, WhatsApp Business, email, in-app chat), normalizing messages to a unified format. Where supported, include quick-action buttons (Confirm, Suggest Alternatives, Add to Calendar) that trigger booking flows without typing; otherwise, fall back to smart-reply guidance and deep links. Ensure message templates, delivery status tracking, and retry logic. Expose webhooks for third-party DMs and a pluggable interface to add new channels without core changes.

Acceptance Criteria
Inbound Message Normalization
Given inbound SMS, WhatsApp Business, email, and in-app chat messages are received When SoloPilot ingests each message Then a normalized payload is produced with fields: channel, external_message_id, sender_handle, thread_id, received_at (ISO 8601 UTC), text, attachments[], cta_capabilities[] And no channel-proprietary fields are present in the emitted payload And the payload is available to downstream booking services within 2 seconds of provider receipt Given an inbound message includes attachments (image or file) When normalized Then each attachment includes type, size_bytes, filename, and a temporary URL valid for at least 15 minutes Given a duplicate delivery (same external_message_id and channel) When processed Then deduplication ensures exactly-once emission to downstream consumers and idempotent persistence
Quick-Action CTA Rendering and Booking Handling
Given a message template includes CTAs: Confirm, Suggest Alternatives, Add to Calendar When sent to WhatsApp Business and in-app chat Then recipients see three interactive buttons labeled accordingly and supported by the channel Given a recipient taps Confirm on a proposed slot When processed Then the appointment is created, a confirmation message is sent on the same channel, and the intake form link is delivered Given a recipient taps Suggest Alternatives When processed Then the system returns three nearest available slots respecting provider working hours and calendar constraints in a single interactive reply, and each option can be booked with one tap Given a recipient taps Add to Calendar after booking When processed Then an .ics file (email or link) and Google/Apple calendar deep links are provided and open a prefilled event Given a channel does not support a specific CTA When rendering Then that CTA is omitted and a fallback instruction is included
Smart-Reply Fallback and Deep Links on Unsupported Channels
Given an SMS or plain-email client that does not support interactive buttons When a template with CTAs is sent Then the message includes smart-reply guidance (e.g., Reply "Confirm" to book, "Alt" for options, or propose a time) and a deep link to the booking page Given a recipient replies with natural language such as "Tuesday 3pm works" When interpreted Then the time is resolved in the recipient's timezone, availability is checked, and the booking is confirmed in one step with a confirmation message returned Given a reply is ambiguous (e.g., "next week" or multiple times) When interpreted Then a clarifying question is sent requesting specific dates/times, and if no response is received within 15 minutes, one reminder is sent before closing the prompt Given a reply explicitly requests alternatives ("alt", "other times") When interpreted Then three valid alternative slots are returned and can be booked by replying with the option number or using the deep link
Message Templates, Personalization, and Localization
Given a message template with variables {client_name}, {slot_time}, {timezone}, {business_name} When rendered for any channel Then variables are populated correctly, date/time is formatted per the recipient locale, and channel-unsafe markup is stripped or downgraded without breaking content Given a recipient locale preference (e.g., en-US, es-ES) When sending a template Then the localized template variant is selected and date/time strings are formatted per locale Given an SMS message exceeds 480 characters after rendering When sending Then the text is segmented, ordered, and labeled to preserve meaning without breaking URLs or deep links Given a template fails validation (missing variables, invalid placeholders) When attempting to send Then sending is blocked and a descriptive error is returned in logs and UI
Delivery Status Tracking and Retry Logic
Given an outbound message is sent When provider callbacks or status APIs are received Then the message status in SoloPilot updates to one of: queued, sent, delivered, read (only where supported), or failed, within 5 seconds of provider event Given a transient failure occurs (HTTP 5xx, network timeout) When sending Then retries use exponential backoff (approximately 2s, 10s, 30s) up to 3 attempts and ensure at-most-once delivery Given a permanent failure occurs (HTTP 4xx not retryable) When sending Then no retries are attempted and the message is marked failed with provider error code and human-readable reason Given a message ultimately fails after max retries When status is updated Then the user is notified in-app with a one-click option to retry or switch channel
Webhooks for Third-Party DMs (Inbound and Outbound)
Given an inbound webhook endpoint is configured for third-party DMs When a POST arrives with a valid HMAC signature, content hash, and idempotency key Then the server responds 200 within 200ms, enqueues processing, and duplicate POSTs do not create duplicate messages Given an inbound webhook request has an invalid or missing signature When received Then the server responds 401 and the payload is discarded Given outbound status webhooks are configured for a partner When a message status changes to sent, delivered, read, or failed Then a signed POST is sent to the partner endpoint within 3 seconds; on 5xx responses, retries occur up to 3 times with exponential backoff
Pluggable Channel Adapter Interface
Given a new channel adapter implementing the interface (send, parseInbound, capability flags: supports_ctas, supports_read_receipts) When registered via the plugin registry and feature flag enabled Then messages can be sent and received through the new channel without modifying core services Given an adapter reports supports_ctas=false When rendering a message with CTAs for that channel Then the system automatically falls back to smart-reply guidance and deep links Given contract tests are executed against the adapter When run in CI Then all interface tests pass including schema conformance for inbound normalization and successful send in sandbox Given the adapter is disabled at runtime When there are in-flight retries Then retries are canceled gracefully, no new sends are attempted, and users are prompted to choose another channel
Identity Matching & Consent Capture
"As a provider, I want replies linked to the correct client and consent recorded so that bookings are accurate and compliant."
Description

Map incoming messages to the correct client record using channel identifiers (phone, email, WhatsApp ID) and verification heuristics. When ambiguous, request lightweight verification (e.g., last name or code) before booking. Record explicit consent for booking actions initiated via messaging and log the message thread as proof. Enforce provider-defined policies (e.g., no bookings from unknown numbers, minors, or blocked contacts). Maintain an auditable trail of who confirmed, when, via which channel, and from which IP/device metadata where available.

Acceptance Criteria
Deterministic Match by Channel Identifier
Given an inbound message arrives via SMS, email, or WhatsApp from an identifier that maps to exactly one active client record And provider policies allow bookings from this contact When the message contains clear booking confirmation intent for an available slot Then the system associates the message with that client and creates/updates the booking within 10 seconds And the system records explicit consent by storing the original message ID, full text, timestamp (ISO 8601), channel, and sender identifier on the booking record And the system sends confirmation and any required intake forms to the client and notifies the provider
Ambiguous Match Requires Lightweight Verification
Given an inbound message arrives where the channel identifier matches multiple client records or the heuristic confidence score is below 0.90 When the system cannot uniquely determine the client Then the system requests lightweight verification (e.g., last name or 4‑digit code) and pauses booking When the correct verification value is received within 15 minutes Then the booking proceeds and the verification method and hashed value are stored with the booking and audit log When verification fails or times out Then no booking is created and the client is informed how to proceed; the attempt and reason are logged
Unknown Sender Blocked per Provider Policy
Given the provider policy is set to disallow bookings from unknown identifiers When an inbound message is received from a phone/email/WhatsApp ID not linked to any active client record Then the system declines to create a booking and responds with a policy message and optional onboarding link And the system logs the attempt with reason "policy_unknown_contact", channel, identifier, and timestamp And no calendar holds or invoices are created
Minor or Age-Restricted Contact Enforcement
Given the provider policy disallows bookings from minors (or below a configured age threshold) And the matched client record indicates an age below the threshold When the client attempts to confirm a booking via messaging Then the system refuses the booking, sends a compliant guidance message (e.g., contact guardian/provider), and logs the policy enforcement And no booking, intake, or invoice is created
Blocked Contact Attempt Rejection
Given a contact is marked as Blocked in the provider's SoloPilot workspace When the blocked contact sends a booking-confirmation message via any supported channel Then the system rejects the request, does not create or modify any booking, and does not send intake forms And the system logs the attempt with reason "policy_blocked_contact" including channel, identifier, and timestamp And the provider receives a non-intrusive notification (configurable)
Consent Capture and Auditable Proof on Message-Initiated Booking
Given any booking is created or updated based on a client message or quick action tap When the booking is finalized Then the booking record includes: who confirmed (client name and ID), channel, original message ID(s), message text snippet, timestamp (ISO 8601 with timezone), match confidence score, verification method (none/last_name/code), and hashed verification value And where available, IP address and device metadata from quick action deep links are captured and attached And the full message thread is linked and exportable as a PDF/JSON audit artifact And the provider can view this consent and thread from the booking details screen
Heuristic Match with Smart Follow-up and Traceability
Given an inbound message has partial identifiers (e.g., shared family phone, name variation) and prior conversation history exists When heuristic signals (prior threads in last 180 days, name similarity, timezone alignment) produce a confidence score ≥ 0.90 Then the system selects the matched client, proceeds with booking, and records the contributing signals and score in the audit log When the score is < 0.90 Then the system issues a smart follow-up asking for a clarifying detail (e.g., last name) before booking And all decision points (scores, prompts, responses) are time-stamped and retrievable
Confirmation, Intake, and Post-Booking Automations
"As a provider, I want confirmed bookings to auto-send confirmations and intake forms so that I don’t have to chase clients or set things up manually."
Description

Upon successful intent resolution and availability check, auto-create the appointment in SoloPilot, send a confirmation message plus a calendar invite, and attach required intake forms based on service type. Trigger existing SoloPilot automations (reminders, pre-session questionnaires, payment requests if applicable). Include reschedule/cancel smart links and support dynamic message templates with personalization tokens. Update the client record and prepare session artifacts (notes template, billing placeholders) to streamline session-to-invoice workflows.

Acceptance Criteria
Auto-create Appointment After Intent and Availability Confirmation
Given a client reply has been resolved to a specific service, date/time, and timezone and the availability check returns the slot as available When the booking is finalized Then an appointment record is created within 5 seconds with fields: clientId, serviceId, startAt (ISO 8601 with timezone), endAt, location, bookingChannel, sourceMessageId, and status "Confirmed" And no overlapping confirmed appointment exists for the same provider/resource at that time And the appointment is associated to the correct provider calendar/workspace And the creation is idempotent based on a deterministic booking key (no duplicate appointments on retries)
Confirmation DM and Calendar Invite Delivery
Given an appointment is created When sending confirmations Then the client receives a confirmation message in the original conversation channel within 5 seconds containing: service name, date/time in client timezone, location/meeting link, reschedule link, and cancel link And an ICS calendar invite is emailed to the client's primary email with organizer set to the provider, including conferencing details and reminder defaults And the provider calendar reflects the new event within 10 seconds And failures are logged and retried up to 3 times with exponential backoff; on persistent failure, the provider is notified
Service-Based Intake Forms Attachment
Given the booked service has required intake forms configured When the confirmation is generated Then secure intake form links (authenticated or one-time token) are created, assigned to the client, and included in the confirmation message and invite description And known client/profile fields are prefilled And the due date is set to 24 hours before session start (or immediate if booked within 24 hours) And intake completion status is visible on the appointment record And access links expire at session start
Trigger Reminders, Questionnaires, and Payment Requests
Given automations are configured for the service When the appointment is confirmed Then reminder jobs are scheduled at the configured offsets (e.g., 48h and 2h before) with the correct channel(s) And a pre-session questionnaire is scheduled to send immediately after confirmation and re-sent at the next reminder if incomplete And if the service requires payment or deposit, a payment request is generated with correct amount, currency, due date, and included link And idempotency ensures only one set of automations and one payment request per appointment even on retries or updates
Reschedule and Cancel Smart Links Functionality
Given the client receives confirmation containing reschedule and cancel links When the client clicks the reschedule link Then they see real-time availability for the same service/provider and can select a new slot And upon rescheduling, the original appointment is updated (not duplicated), automations are recalculated to the new time, and updated confirmations and invites are sent When the client clicks the cancel link Then the appointment status becomes "Canceled", cancellation policy rules are applied (fees/refunds as configured), and both parties are notified And the links are single-appointment scoped, single-use, and expire at session start
Dynamic Message Templates With Personalization Tokens
Given a confirmation template contains tokens such as {client.firstName}, {service.name}, {session.datetime}, {session.location}, {rescheduleLink}, {cancelLink}, {intakeLinks}, {paymentLink} When generating the confirmation message and invite description Then all tokens resolve from client, service, and appointment data with locale-aware date/time and timezone formatting And missing optional fields use safe fallbacks (e.g., {client.firstName} -> "there") without exposing raw tokens And unknown tokens are removed and logged as warnings And the final message contains no unresolved braces and respects channel limits (e.g., <= 1000 chars for SMS)
Client Record Update and Session Artifacts Preparation
Given the appointment is confirmed When post-booking tasks run Then the client record is updated with lastBookedAt, upcomingSessionsCount, and a service history entry referencing the appointment And a notes template appropriate to the service is created and linked to the appointment And billing placeholders are created with line items per service pricing, duration, and tax configuration, enabling one-click session-to-invoice And the end-to-end post-booking workflow completes within 10 seconds and is idempotent across retries
Admin Controls, Policies, and Templates
"As a provider, I want to configure how Reply-to-Book behaves across my services and channels so that it matches my policies and brand."
Description

Offer configuration for enabling Reply-to-Book per service and channel, setting default follow-up questions, confirmation templates, office hours, buffers, lead/notice periods, and reschedule/cancellation rules. Provide per-service and per-channel toggles, branding options, and language/locale settings. Allow overrides for VIP clients and exceptions. Include RBAC so only authorized roles can change policies. Provide a sandbox/preview mode to test flows before enabling in production.

Acceptance Criteria
Per-Service and Per-Channel Enablement
Given a workspace has Service A with channels Email, SMS, WhatsApp, and In-App DM, When an Admin toggles Reply-to-Book ON for SMS for Service A and OFF for other channels, Then only SMS inbound replies for Service A are interpreted and auto-booked. Given Reply-to-Book is OFF for a channel, When a client replies on that channel, Then the system does not attempt NLP parsing and sends the configured fallback message for disabled channels. Given Reply-to-Book is ON for a channel, When a client taps a quick action in that channel's DM, Then the booking flow is initiated for the configured service and channel. Given a service is archived or paused, When viewing its channel toggles, Then toggles are disabled and Reply-to-Book is inactive for that service.
Default Follow-Up Questions and Clarifications
Given an Admin defines default follow-up questions for missing duration and location for Service A, When a client reply lacks duration or location, Then the system sends the configured follow-up prompts in order until required fields are provided. Given validation rules are set for follow-up answers, When a client reply is ambiguous (e.g., "later afternoon"), Then the system sends the mapped clarification prompt and does not book until a valid value is supplied. Given a maximum follow-up attempts threshold of 2 is configured, When the threshold is exceeded, Then the system sends the escalation/fallback template and stops auto-booking.
Confirmation Templates, Branding, and Locale
Given an Admin sets per-channel confirmation templates with placeholders {client.first_name}, {service.name}, {start_time_local}, and selects Brand Theme X and locale fr-FR, When a booking is confirmed via SMS, Then the rendered message uses Theme X branding, is localized to fr-FR, and placeholders resolve with booking data. Given a placeholder lacks data and a default is provided (e.g., {location|TBD}), When rendering a confirmation, Then the default value is used without error. Given channel-specific length limits (e.g., SMS 160 characters per segment) are defined, When rendering a confirmation, Then the preview shows segment count and outbound messages are sent split without truncation. Given time zone differences between provider and client, When rendering {start_time_local}, Then the time is shown in the client's locale and time zone while honoring the provider's calendar time.
Office Hours, Buffers, and Lead Time Enforcement
Given office hours are set to Mon–Fri 09:00–17:00 and a 30-minute buffer before and after sessions, When a client proposes "Tuesday 5pm", Then only slots that start within office hours and respect buffers are considered bookable. Given a minimum lead time of 24 hours and a maximum scheduling window of 60 days are configured, When a client proposes a time within 8 hours from now or beyond 60 days, Then the system declines and proposes the next available times within policy. Given daylight saving transitions affect the provider's time zone, When computing availability for a client in a different time zone, Then suggested times adhere to provider office hours and are displayed in the client's local time.
Reschedule and Cancellation Rules
Given Service A has reschedule cutoff 12h and cancellation cutoff 24h with fee policy "charge 50% inside cutoff", When a client requests a change inside these windows, Then the system enforces the rule (block, require approval, or apply fee) and sends the corresponding template. Given an exception rule "waive first violation" is configured, When a first-time client cancels inside cutoff, Then the fee is waived and the exception is recorded. Given approvals are required for reschedules inside cutoff, When a client requests reschedule within 6 hours, Then the request is set to pending approval, no calendar changes are made, and both parties are notified.
VIP Overrides and Exceptions
Given a client is marked VIP with overrides "ignore lead time" and "allow after-hours", When the VIP replies "tonight 8pm", Then the system surfaces slots outside office hours and bypasses lead-time checks only for that client. Given VIP overrides are limited to Service A, When the VIP requests Service B without overrides, Then standard policies apply. Given override usage must be auditable, When an appointment is booked or changed due to a VIP override, Then an audit log entry captures client, policy bypassed, actor, timestamp, and change details.
RBAC, Audit, and Sandbox/Preview Mode
Given roles Owner, Admin, and Staff exist and only Owner/Admin have "Configure Reply-to-Book Policies", When a Staff user attempts to save a policy change, Then the save is blocked with a 403 error and an in-UI permission notice. Given a privileged user updates any policy or template, When the change is saved, Then an immutable audit record is created with user, timestamp, environment (Sandbox/Production), fields changed, and old/new values. Given Sandbox mode is enabled, When an Admin uses "Preview flow" to simulate a client reply "Tuesday 3pm" for Service A, Then the system runs the full flow in isolation (no production calendar writes or messages), renders the client/host messages, and shows what would be booked. Given "Promote from Sandbox" is used, When the Admin confirms promotion, Then the sandbox configuration is copied to Production with versioning and a success confirmation, leaving sandbox data intact.
Observability, Error Handling, and Feedback Loop
"As a provider, I want visibility into bookings and tools to correct misinterpretations so that the system gets more accurate over time."
Description

Implement end-to-end tracing and structured logs for every message-to-booking flow, with metrics on parsing accuracy, time-to-book, channel conversion, and failure reasons. Surface an inbox view showing the parsed intent, confidence, and decision path, with a one-click correction UI that retrains or updates parsing rules. Provide alerts for repeated failures or channel outages and automatic fallbacks (e.g., send a booking link) when confidence or connectors fail. Support exportable audit logs and privacy controls for message retention and redaction.

Acceptance Criteria
Traceability of Message-to-Booking Flow
Given a client reply arrives via any supported channel When the message is processed through ingestion, parsing, availability lookup, booking, and notification Then a single correlation_id is generated and propagated to all spans and structured logs And 99% of flows (success and failure) contain spans for each stage with start and end timestamps And each trace includes tenant_id, workspace_id, channel, message_id, client_id (pseudonymous), intent, confidence, decision, outcome, error_code, retry_count, and booking_id when created And p50, p95, and p99 end-to-end time-to-book metrics are emitted per tenant and per channel And on retries, spans are linked to the original correlation_id with attempt numbers And traces are queryable by correlation_id within 5 minutes of event time
Structured Logging with Redaction and Retention Controls
Given structured logging is enabled for a workspace When events are written Then logs are emitted as JSON with schema_version, event_type, event_time (UTC), correlation_id, message_id, tenant_id, channel, and outcome fields And raw message content is stored only when message_retention=true for the workspace And when message_retention=false, raw message bodies are irreversibly redacted before persistence and within 5 minutes of ingestion And redaction removes emails, phone numbers, and free-text names, replacing with consistent tokens, and sets redaction_flags accordingly And retention period is configurable per workspace (7–365 days) with default 90 days, and data older than the period is purged daily And a Delete-My-Data request for a client identifier purges related messages and logs within 24 hours and creates an audit entry And only Owner and Support Admin roles can view unredacted content when retention=true
Metrics for Parsing Accuracy, Time-to-Book, Conversion, and Failures
Given metric collection is enabled When conversations progress from first reply to booking or failure Then parsing intent accuracy and slot-fill accuracy are computed daily from correction labels and a held-out set And last-30-day English intent accuracy >= 90% and slot accuracy >= 85% per channel with at least 500 samples And time-to-book is measured from first client reply to booking creation; p50 <= 20s and p95 <= 90s per channel And channel conversion rate (%) is computed as bookings / eligible conversations within 24h and reconciles within 1% of the bookings database And failure reasons are categorized using a controlled taxonomy; “unknown” category accounts for < 5% of failures And all metrics are visible in a dashboard with filters (date range, channel, tenant) and update latency <= 5 minutes And metrics are exportable as CSV via UI and API
Inbox Decision Path with One-Click Correction
Given a user opens the Reply-to-Book inbox for a conversation When the item is displayed Then it shows parsed intent, extracted slots (date, time, duration), confidence [0–1], and the decision path steps that led to the outcome And 95% of inbox items load in <= 1s and render the decision path in <= 500ms after data fetch When the user applies a one-click correction to intent and/or slots Then the system reprocesses the conversation within 10s, updates the outcome, and optionally sends a corrected client message And the correction is recorded in an immutable audit log with user_id, before/after values, timestamp, and correlation_id And a training example is created; rules updates apply immediately and model updates are applied within 30 minutes And only users with the “Can Correct Parsing” permission can apply corrections And the user can undo a correction within 5 minutes; any client message sent due to the correction is retracted or clarified if the channel supports it
Alerts for Failures/Outages and Automatic Fallbacks
Given operational monitoring is active When 5 or more parse or booking failures occur for a tenant within 10 minutes or the failure rate exceeds 10% over 30 minutes Then an alert is sent to the tenant’s configured channels (email, Slack, webhook) within 60 seconds including top failure reason and three sample correlation_ids When the connector error rate for a channel exceeds 20% for 5 minutes Then the channel is marked degraded, a status event is emitted, and alerts are sent When intent confidence < 0.75 or a required slot is unresolved after one clarification Then the client is sent a booking link prefilled with known details, the operator is notified in the inbox, and the fallback is logged When a connector send fails or times out Then the system retries with exponential backoff up to 3 attempts with idempotency keys and falls back to an alternate channel or email if available And the system guarantees exactly-once booking confirmation or booking-link delivery per conversation
Exportable Audit Logs API and UI
Given an authorized user requests an export of audit logs When filters (date range, tenant, channel, outcome, confidence range) are applied and format (NDJSON or CSV) is selected Then the export includes event_time (UTC), correlation_id, message_id, intent, confidence, decision, outcome, error_code, booking_id, redaction_flags, and actor metadata And redacted content is excluded or replaced with “[REDACTED]” unless the workspace allows retention and the requester has permission And exports up to 100,000 rows are generated within 2 minutes; larger exports up to 1,000,000 rows stream with pagination And the download is provided via a pre-signed URL valid for 24 hours and includes a SHA-256 checksum; the export request is logged immutably And the API enforces a limit of 5 concurrent exports per tenant and returns 429 when exceeded

Signature Enrich

Scrapes email signatures and social profiles to auto-create complete contacts—name, phone, company, timezone, and locale—linked to the thread. Eliminates manual entry and keeps records accurate for billing, reminders, and compliance.

Requirements

Signature Parsing Engine
"As a solo practitioner, I want email signatures automatically parsed into contacts so that I don’t waste time on manual data entry and can trust my records for billing and reminders."
Description

Implement a server-side parser that detects and extracts structured contact data from email signatures on inbound and outbound messages. Parse name, title, company, phone(s), email, website, physical address, and optional cues (timezone tokens, locale indicators). Normalize and validate data (e.g., E.164 for phones, RFC 5322 for emails), strip legal disclaimers/footers, and handle multi-language signatures and forwarded/replied content. Run on message ingestion with idempotency keys tied to message IDs. Output field-level confidence scores and provenance (signature vs. header). Seamlessly integrates with SoloPilot’s contact model to auto-populate new or existing contacts and link them to the originating thread for downstream scheduling, invoicing, reminders, and automations.

Acceptance Criteria
Inbound Email With Standard Signature
Given an inbound message containing a conventional signature block with name, title, company, phone, email, website, and physical address When the message is ingested by the Signature Parsing Engine Then each field is detected and extracted into structured fields And phone numbers are normalized to E.164 format And email addresses validate against RFC 5322 And websites are normalized to include scheme and punycode where needed And physical addresses are parsed into street, city, region/state, postal code, and country And field-level confidence scores in the range [0.0, 1.0] are returned And field-level provenance is set to "signature" for signature-derived values And a new contact is created in the SoloPilot contact model populated with the extracted fields And the contact is linked to the originating message thread
Multi‑Language Signature Parsing With Locale/Timezone Cues
Given an inbound message whose signature is written in a non-English language using UTF-8 and locale-specific labels (e.g., "Tél.", "Empresa", "Dirección") possibly including timezone tokens (e.g., "PST", "GMT+1") When the message is ingested by the Signature Parsing Engine Then name, title, company, phone, email, website, and address are correctly detected despite non-English labels And Unicode characters (e.g., accents/diacritics) are preserved in stored fields And phone numbers are normalized to E.164 and emails validate against RFC 5322 And address components are parsed respecting locale conventions where applicable And if locale indicators are present, the contact locale is set to a valid BCP 47 tag with a confidence score; otherwise locale is left unset And if timezone tokens are present, the contact timezone is set to a valid IANA timezone with a confidence score; otherwise timezone is left unset And field-level provenance is set to "signature" for signature-derived values
Footer and Legal Disclaimer Stripping
Given a message that includes a signature followed by legal disclaimers, marketing banners, or unsubscribe footers When the message is ingested by the Signature Parsing Engine Then non-signature sections (disclaimers, banners, unsubscribe text) are excluded from parsing And no disclaimer or marketing text is persisted into any contact field And no confidence score is emitted for excluded sections And signature fields, if present, are still extracted and stored as structured data with provenance "signature"
Outbound Message Parsing With Quoted Recipient Signature
Given an outbound reply or forward sent from SoloPilot that includes the recipient's prior signature in quoted content When the message is ingested by the Signature Parsing Engine on send Then the engine extracts contact data from the most recent recipient signature present in the quoted content And the engine does not extract data from the SoloPilot user's own signature And extracted fields carry provenance "signature" and are integrated into the contact model And the contact is created or updated and linked to the associated outbound thread
Forward/Reply Chain Signature Selection
Given a message containing multiple quoted replies/forwards with several historical signatures When the message is ingested by the Signature Parsing Engine Then only the most recent relevant signature closest to the top of the thread is considered for extraction And older signatures in deeper quoted sections are ignored And quoted original message headers (e.g., "From:", "Sent:") are not misclassified as signature fields And exactly one contact create/update operation is performed for this message
Idempotent Processing By Message ID
Given that the same message (same provider message ID) may be delivered or retried multiple times When the message is ingested multiple times with the same idempotency key derived from the message ID Then parsing results are applied at most once And no duplicate contacts are created And no duplicate phone/email entries are added to an existing contact And subsequent ingestions return the same results without additional side effects
Existing Contact Merge With Confidence and Provenance
Given parsed signature fields that match an existing contact by normalized email or phone When integrating parsed results into the SoloPilot contact model Then fields are updated only if the new value has a higher confidence score than the stored value or the stored value is null And field-level provenance is stored as provided ("signature" or "header") per field And normalized matching prevents duplicate phone/email entries And the contact remains linked to the originating thread after the update
Profile Enrichment APIs
"As a consultant, I want contacts auto-enriched from public profiles so that I have complete, up-to-date details without manual research."
Description

Augment parsed signature data with public profile enrichment via approved third-party APIs (e.g., company domain lookup, social profiles) using email and domain as keys. Respect vendor ToS, rate limits, and privacy requirements. Cache results with TTL, implement exponential backoff/retry, and maintain per-vendor credentials in secure storage. Enrich missing attributes such as company size/industry, job title, LinkedIn URL, and city/region for improved timezone inference. Store field-level provenance and confidence along with timestamps for refresh governance. Integrate with SoloPilot automations by enriching asynchronously and emitting events when contact records are updated.

Acceptance Criteria
Asynchronous Enrichment Populates Missing Contact Fields
Given a contact is created from a parsed email signature containing a valid email address and company domain When the enrichment worker processes the contact asynchronously Then the system queries only approved vendors using email and domain as keys And fills only missing fields among: job_title, company_size, company_industry, linkedin_url, city, region And stores each populated field with provenance {vendor, method, fetched_at} and a confidence score between 0.0 and 1.0 And updates the contact record in the database And emits a contact.enriched event with the changed fields and their provenance And completes within 120 seconds under nominal load
Rate Limiting, Backoff, and Retry Compliance
Given a vendor returns HTTP 429 or a request exceeds configured rate limits When the enrichment worker schedules subsequent requests Then exponential backoff with jitter is applied starting at 1s and doubling up to a max delay of 120s And Retry-After headers are honored when present And retries are capped at 3 attempts per vendor per contact per run And the system does not exceed the configured per-vendor requests-per-minute threshold And structured logs and metrics record throttling, retries, and outcomes And the attempt is marked deferred when retries are exhausted
Per-Vendor Credential Security and Isolation
Given vendor API credentials are configured When the enrichment worker authenticates to a vendor Then credentials are retrieved from secure storage at runtime and never hardcoded or logged And credentials are encrypted at rest and in transit And access to credentials is audited with timestamp, actor, and purpose And credentials are isolated per vendor and environment (dev, stage, prod) And credential rotation is supported without downtime via versioned secrets
Caching with TTL and Field-Level Refresh Governance
Given an enrichment response exists in cache for a contact When a new enrichment is requested within the configured TTL (e.g., 30 days) Then cached values are returned and no vendor calls are made And each field retains its own fetched_at timestamp and TTL When the TTL for a specific field expires Then only that field is re-queried from vendors And existing values are not overwritten by lower-confidence results unless a manual override flag is set And last_refresh_at and next_refresh_at are recorded per field
Privacy, Consent, and Vendor ToS Enforcement
Given a contact or workspace has do_not_enrich=true or resides in a restricted region When an enrichment job is triggered Then the job is skipped and the reason is recorded without calling vendors And no personal data is sent to vendors outside the configured allowlist And vendor ToS requirements (e.g., attribution or purpose headers) are satisfied on each request And a compliance log entry is created for each vendor call with purpose and fields requested
Idempotent Updates and Event Delivery
Given an enrichment job runs multiple times for the same contact due to retries or schedules When no field values would change Then the contact record is not updated and no event is emitted When at least one field value changes Then updates are applied using an idempotency key per contact+vendor+payload hash And a single contact.enriched event is emitted containing only changed fields, provenance, confidence, and a correlation_id And downstream consumers can deduplicate via the idempotency key
Timezone and Locale Inference from Enriched Data
Given city and region are enriched and a company domain TLD is available when applicable When timezone and locale inference runs Then contact.timezone is set using a maintained mapping with at least 95% coverage of supported cities And contact.locale is set from locale hints (email headers, domain TLD, or vendor-provided locale) when available And user-specified timezone or locale is never overwritten And timezone_source and timezone_confidence are stored in provenance And a contact.timezone_inferred event is emitted only if the timezone changes
Contact Creation & Deduplication
"As a therapist, I want new contacts created and linked to the email thread without duplicates so that my client list stays clean and workflows run reliably."
Description

Create or update SoloPilot contacts based on parsed/enriched data with robust deduplication. Match primarily on email, secondarily on phone and fuzzy name+company, with tunable thresholds. Merge records safely using field precedence rules and confidence scores; never overwrite user-locked fields. Ensure thread linking: associate the email thread/conversation ID with the contact and client workspace. Provide atomic upsert operations, concurrency safety, audit trails, and rollback. On success, trigger downstream workflows (e.g., session-to-invoice, reminders) using the enriched contact profile.

Acceptance Criteria
Primary Email Upsert and Thread/Workspace Linking
Given a parsed/enriched contact payload containing email E and email thread ID T from workspace W When an upsert is requested and no existing contact has email E Then a new contact is created with email=E, the contact is linked to thread T and workspace W, and the operation returns contact_id and status=created Given a parsed/enriched contact payload containing email E and email thread ID T from workspace W and an existing contact with email E exists When an upsert is requested Then that contact is updated per field precedence rules, thread T is associated to the contact if not already, the operation returns the existing contact_id and status=updated, and no additional contact is created Given the same payload is submitted multiple times with the same idempotency key K within 60 seconds When upserts are requested Then exactly one create/update occurs and all responses include the same contact_id and status=idempotent
Phone Fallback Matching Without Email
Given a payload without email but with phone P When an upsert is requested Then phone P is normalized to E.164 and matching is performed on the normalized phone Given normalized phone P uniquely matches an existing contact When an upsert is requested Then that contact is updated per precedence rules and the operation returns contact_id and status=updated Given normalized phone P matches more than one contact When an upsert is requested Then no automatic merge occurs, a new contact is not created, the operation returns status=ambiguous with the list of candidate contact_ids, and an audit entry records the ambiguity Given normalized phone P matches no existing contact When an upsert is requested Then a new contact is created with phone=P and the operation returns contact_id and status=created
Fuzzy Name+Company Matching with Tunable Thresholds
Given a payload with name N and company C but no email or phone and the fuzzy threshold is set to 0.85 When an upsert is requested Then a similarity score S is computed against existing contacts using name+company and both S and the threshold are recorded in the audit Given S >= 0.85 and the top match is unique When an upsert is requested Then the top-matched contact is updated per precedence rules and the operation returns contact_id and status=updated Given S < 0.85 When an upsert is requested Then a new contact is created and the operation returns contact_id and status=created Given the threshold is changed to 0.90 via settings When a subsequent upsert with the same payload is requested Then the new threshold 0.90 is used in the decision and the decision outcome reflects the new threshold Given multiple contacts have scores within 0.02 of the top score and >= threshold When an upsert is requested Then no automatic merge occurs and the operation returns status=ambiguous with candidate contact_ids
Field Precedence and User-Locked Protection
Given field precedence rules user_locked > user_entered > enriched > parsed > empty When a merge occurs Then any user_locked field value remains unchanged and no lower-precedence source overwrites a higher-precedence value Given a target contact has blank timezone and the incoming payload supplies timezone TZ from enriched data When a merge occurs Then contact.timezone is set to TZ and field_provenance.timezone=enriched Given a target contact has locale L1 from user_entered and the incoming payload supplies locale L2 from parsed When a merge occurs Then locale remains L1 and an audit entry records that L2 was ignored due to precedence Given conflicting phone formats representing the same number When a merge occurs Then the stored phone value is normalized to E.164 and duplicates are collapsed to a single canonical entry Given a user locks a field after a merge When a subsequent upsert attempts to change that field Then the value is not changed and the audit records a protected_field_skip
Atomic Upsert and Concurrency Safety
Given two upsert requests for the same email E arrive within 100 ms When processed concurrently Then exactly one contact is created or updated, both requests return the same contact_id, and no duplicate records exist Given an upsert touches multiple tables (contacts, contact_threads, audit) When a downstream write fails Then the entire transaction is rolled back, the contact state remains as before the upsert, and an audit entry with status=failed and reason is recorded Given version V of a contact record When an upsert is applied Then the record version increments to V+1 and subsequent concurrent upserts with stale versions are retried or rejected with a 409 conflict Given high concurrency (>= 50 parallel upserts for the same email E over 10 seconds) When processed Then p99 upserts complete without deadlocks and the final state reflects a single consistent record
Audit Trail and Rollback
Given a successful upsert or merge When the operation completes Then an audit log is persisted containing contact_id, actor=system, source=Signature Enrich, timestamp, thread_id, workspace_id, fields_changed with before/after/provenance, match_strategy, confidence_scores, decision, and idempotency_key Given an audit entry A for contact C When a rollback to A is requested by an authorized user Then all fields revert to the values recorded in A, field provenance is restored, and a compensating audit entry is created with action=rollback referencing A Given a rollback is executed When downstream systems had been triggered by the original upsert Then compensating events are emitted to prevent duplicate billing/reminders and no additional duplicates are created Given an audit retention policy of at least 365 days When queried Then audit entries for contact C within that period are retrievable and immutable
Downstream Workflow Triggers and Idempotency
Given a successful create or update with a non-ambiguous decision When the upsert transaction commits Then downstream workflows (e.g., session-to-invoice, reminders) are triggered with the enriched contact profile and workspace context, and each trigger includes a correlation_id tied to the upsert Given the same upsert is retried with the same idempotency key K When processed Then no duplicate downstream workflows fire and responses return status=idempotent with the original correlation_id Given a downstream dispatch temporarily fails When retries occur Then exponential backoff is applied up to N attempts and on eventual success only one workflow instance is active, with failure and retry_count recorded in audit Given the upsert outcome is status=ambiguous When processing triggers Then no downstream workflows are emitted
Timezone and Locale Inference
"As a coach, I want timezone and locale auto-detected so that scheduling and reminders are sent at the right time and in the right format for each client."
Description

Infer contact timezone and locale using a layered approach: signature tokens (e.g., PT, CET), enriched profile location, email header Received paths, and phone country codes. Map to IANA tz database identifiers and BCP 47 language tags. Provide confidence scoring and fallbacks (e.g., company HQ) with a threshold for auto-apply; otherwise route to review. Update SoloPilot scheduling defaults, reminder send-times, and invoice due-date localization using inferred values. Persist provenance to support audits and future re-computation if better signals arrive.

Acceptance Criteria
Confidence Scoring and Threshold Auto-Apply
Given multiple signals are available (signature tokens, enriched profile location, email headers, phone country code) When the inference engine computes timezone and locale hypotheses Then it assigns a confidence score in the range [0.0, 1.0] to the top hypothesis And if the score >= 0.80 the system auto-applies the inferred IANA timezone and BCP 47 locale to the contact And if the score < 0.80 the system makes no changes and routes the contact to Review with candidate values and scores attached
Signature Token Parsing and Disambiguation
Given an email signature contains timezone indicators (e.g., "PT", "PST", "PDT", "CET", "CEST") or city/region names When the system parses the signature Then recognized indicators are normalized with date-aware DST rules and mapped to a single IANA timezone And ambiguous indicators (e.g., "CST") are not auto-applied unless corroborated by another signal; otherwise they are routed to Review And if timezone is resolved from signature, locale is inferred from the resolved country as a BCP 47 tag unless a stronger locale signal exists
Profile Location Mapping to IANA and BCP 47
Given the enrichment service returns a contact location (city/region/country) with geocodes When the system processes the location Then it maps the location to a single IANA timezone for that locality And it infers a BCP 47 locale using country and known language signal; if none, the country default language is used And this signal contributes to scoring with higher weight than email header offset but lower than explicit signature timezone
Email Header Offset Extraction and Use
Given an email contains one or more Received headers with timestamps and UTC offsets When the system parses the headers Then it extracts the sender-side UTC offset from the earliest hop attributable to the sender And offset-only inference is never auto-applied without corroboration from at least one other signal And when corroborated, the system maps the offset + inferred country to a representative IANA timezone for scoring
Phone Number Country Code Inference
Given a verified E.164 phone number is present for the contact When the system parses the number Then it derives the country from the country code And it infers the locale as the country’s primary BCP 47 language tag unless a stronger locale signal exists And for single-timezone countries it auto-applies the IANA timezone; for multi-timezone countries it contributes to scoring but does not auto-apply without subnational corroboration
Apply Inferred Values to Scheduling, Reminders, and Invoices
Given a contact has auto-applied timezone and locale When a new appointment is created for the contact Then the appointment’s default timezone equals the contact’s IANA timezone And reminder send-times are scheduled at the configured local time in the contact’s timezone And invoices for the contact display and parse dates using the contact’s locale-specific format and localized labels where variants exist
Provenance Persistence and Re-computation
Given inferred timezone and/or locale values are generated for a contact When the system persists the inference Then it stores provenance including sources used, per-signal weights/scores, mapping versions, threshold used, timestamp, and reviewer identity if applicable And when new signals arrive or mapping catalogs are updated Then the engine recomputes; if the new top hypothesis differs and score >= 0.80 it auto-updates and appends an audit record; if score < 0.80 it retains current values and creates a Review task And audit history is queryable per contact with complete change chronology
Review Queue with Confidence Scoring
"As a freelancer, I want to review uncertain contact updates so that bad data doesn’t corrupt my records or trigger wrong reminders."
Description

Introduce a review UI and backend workflow for low-confidence or conflicting fields before committing changes. Show proposed values, source provenance, and diffs vs. existing contact data. Support approve, edit, reject, and merge actions with bulk operations. Notify users when items need review and log decisions for auditability. Feed approved corrections back into parsing heuristics as training hints. Only high-confidence fields auto-apply; medium/low-confidence items await review to protect data quality.

Acceptance Criteria
Auto-Apply High-Confidence Fields Without Review
Given a workspace high-confidence threshold is configured (default 0.90) and a parsed contact contains fields with confidence scores When ingestion completes Then any field with confidence >= threshold is applied to the contact record immediately and does not appear in the review queue And no field with confidence < threshold is auto-applied And fields marked locked/verified on the contact are not overwritten by auto-apply And an Auto-applied audit event is recorded with field, value, score, source, and timestamp
Queue Medium/Low-Confidence Items for Review
Given thresholds are configured (high >= 0.90, medium 0.60–0.89, low < 0.60) and parsing yields fields below the high threshold When ingestion completes Then each field below the high threshold appears as a review item grouped by contact within 60 seconds And duplicate proposals for the same field from the same source are deduplicated And conflicting proposals from different sources are grouped into a single compare-and-choose item
Review UI: Proposed Values, Provenance, and Diffs
Given a contact has existing data and one or more proposed values for fields When a reviewer opens the review item Then the UI shows for each field: existing value vs proposed value side-by-side with inline diff highlighting And displays source provenance (source type, origin link, captured timestamp) and confidence score And displays a link to the related email thread or social profile when available And shows a per-field change preview indicating overwrite, create, or no-change
Single-Item Actions: Approve, Edit, Reject, Merge
Given an open review item When the reviewer clicks Approve Then the selected proposed values are written to the contact and the item is removed from the queue And an audit entry is created with decision=approved and before/after values Given an open review item When the reviewer edits a proposed value and clicks Save Then the edited value is written and recorded as reviewer_edited And the edited value is used for training feedback Given an open review item with conflicting proposals When the reviewer selects Merge Then the reviewer can choose per-field from among proposals or keep existing And the chosen values are persisted in one commit Given an open review item When the reviewer clicks Reject Then no contact data is changed and the item is removed from the queue And a reason can be selected/entered and stored
Bulk Operations and Partial Failure Handling
Given N review items are selected (N <= 500) When the reviewer triggers a bulk Approve, Reject, or Merge Then each item is processed atomically per item with a visible progress indicator And a completion summary shows counts of succeeded, failed, and skipped And failed items remain in the queue with an error message and no partial changes applied And bulk processing throughput is at least 100 items per minute under normal load
Notifications for Pending Review Items
Given new items enter the review queue and the user has review permissions When items are available for review Then the in-app Review badge updates within 60 seconds to reflect the current open count And a daily email digest is sent at 5pm local time if one or more items remain pending And clicking the notification opens the Review Queue filtered to pending items
Auditable Decision Log and Training Feedback Loop
Given any Approve, Edit, Reject, or Merge decision is made When the action is completed Then an immutable audit log entry is stored with reviewer_id, timestamp, field, before_value, after_value, confidence, source, decision, and optional reason And the audit log is retained for at least 1 year and is exportable to CSV Given an Approve or Edit action occurs When the change is saved Then a training hint event is emitted to the parsing heuristics service with field_name, old_value, new_value, source_type, confidence_before, decision_type, and contact metadata And the event is acknowledged by the heuristics service and retried with exponential backoff on transient failure
Privacy, Consent, and Compliance Controls
"As a business owner, I want enrichment to comply with privacy laws and client consent so that I can operate confidently without legal risk."
Description

Embed GDPR/CCPA-compliant processing for enrichment activities. Provide workspace-level controls to enable/disable enrichment, define allowed sources, and honor per-contact opt-outs and “do not enrich” flags. Minimize data collection, encrypt PII in transit/at rest, and maintain processing logs with purposes and retention windows. Support DSAR export/delete and field-level provenance. Enforce robots/ToS compliance, respect do-not-track signals where applicable, and restrict enrichment for sensitive domains or minors. Surface a compliance report per workspace for audits.

Acceptance Criteria
Workspace Enrichment Master Switch and Source Allowlist
Given I am a workspace admin When I toggle Signature Enrich to Disabled Then no enrichment network requests are initiated for any inbound or existing threads And all scheduled enrichment jobs are skipped with reason "enrichment_disabled" in processing logs And a workspace audit log entry records actor, timestamp, and previous/new value Given Signature Enrich is Enabled with an allowlist of sources S When enrichment runs Then only sources in S are queried And calls to disallowed sources are blocked server-side and not persisted And each block is logged with reason "source_not_allowed" Given the allowlist is modified When I save changes Then the change is versioned in audit logs with a diff of added/removed sources And the new rules apply to all new jobs within 1 minute Given enrichment is Disabled When a user attempts a manual enrich on any contact Then the action is not executed and the UI shows "Enrichment is disabled by workspace policy"
Per-Contact Opt-Out and Do-Not-Enrich Enforcement
Given a contact has the Do Not Enrich flag set via UI or API When any enrichment job is created for that contact Then the job is not executed And no network calls are made to enrichment sources And the skip is logged with reason "do_not_enrich" Given a contact previously enriched is set to Do Not Enrich When future emails arrive or a manual enrich is attempted Then existing values remain unchanged by automation And the contact header shows "Enrichment disabled for this contact" Given the Do Not Enrich flag is removed When the next enrichment trigger occurs Then enrichment resumes per current workspace rules
Data Minimization and Field-Level Provenance
Given enrichment is enabled When data is collected Then only the following fields are stored: name, phone, company, timezone, locale, email, profile URL(s) And any additional scraped data is discarded before storage Given any field is enriched or updated When the value is saved Then a field-level provenance record is stored including source identifier, retrieval timestamp, processing purpose, collection method, and retention end date Given a user views a contact When inspecting an enriched field Then the provenance details are visible on demand (hover/click) without exposing unrelated PII Given a retention window is configured for enrichment data When a field’s retention end date is reached Then the field value and its provenance are purged And the purge action is recorded in the audit log with counts
Encryption of PII In Transit and At Rest
Given PII is transmitted between services When inspecting network traffic Then all requests use TLS 1.2+ with strong ciphers And plaintext requests are blocked and logged Given PII is stored in databases or backups When reviewing the storage configuration Then data is encrypted at rest using KMS-managed keys (AES-256 or equivalent) And backups inherit the same encryption Given an unauthorized role attempts to access stored enrichment payloads When the request is made Then access is denied by RBAC And the attempt is logged with user, timestamp, and resource
Processing Logs with Purpose and Retention
Given an enrichment attempt occurs When processing begins and ends Then a processing log entry is created with contact ID, purpose ("contact_enrichment"), source, fields touched, start/end timestamps, outcome (success/skip/fail), and retention end date Given processing logs reach their retention end date When the retention job runs Then expired entries are deleted or anonymized per policy And a deletion summary is recorded in the audit log Given an admin filters or exports processing logs When a CSV or JSON export is requested Then the export contains entries matching the filter And excludes raw PII values while retaining field names and provenance metadata
DSAR Export/Delete and Workspace Compliance Report
Given an admin submits a DSAR export for a contact When the request is processed Then a machine-readable package (JSON and/or CSV) containing the contact’s stored PII, field-level provenance, and processing logs is produced within 15 minutes And the export event is audit-logged Given an admin submits a DSAR delete for a contact When the request is processed Then all stored PII, provenance, and processing logs for that contact are purged within 24 hours except where legal retention is required And the contact is marked Do Not Enrich And a deletion receipt is recorded Given an admin opens the workspace Compliance Report When the report loads Then it displays enrichment enablement state, allowed sources, DNT handling, robots/ToS enforcement status, retention configuration, DSAR counts/statuses, and last audit timestamp And the report can be exported to PDF and JSON
Robots/ToS, Do-Not-Track, and Sensitive Domain/Minor Restrictions
Given a target domain’s robots.txt or meta robots disallows scraping endpoints used for enrichment When an enrichment attempt targets that domain Then no request is made And the job is skipped with reason "robots_disallow" in processing logs Given a source’s Terms of Service disallow automated collection When an admin attempts to add that source to the allowlist Then the system blocks enablement with a compliance warning And records an audit entry if an admin override is used Given an outbound request is made to a site that supports DNT/GPC When the request is sent Then the request includes DNT: 1 and GPC headers And if the site indicates tracking is disallowed, the job is skipped and logged with reason "do_not_track" Given a contact belongs to a restricted domain (e.g., configured blocklist such as .k12/.edu) or has a Minor flag set When enrichment is triggered for that contact Then enrichment is skipped And the skip is logged with reason "restricted_category" And no data is fetched or stored
Admin Field Mapping and Overrides
"As a workspace admin, I want control over how enriched data maps into our contact fields so that automations and invoices use the correct information."
Description

Provide settings for admins to map parsed/enriched fields to SoloPilot’s contact schema and any custom fields. Define field precedence (manual > verified enrichment > signature), lock fields from auto-updates, and configure normalization rules (e.g., phone formatting, title casing). Allow per-field update frequency and TTLs to limit churn. Include test tools to run samples and preview outcomes before applying. Ensure mappings propagate to billing, reminders, and compliance modules consistently.

Acceptance Criteria
Field Mapping to Contact Schema and Custom Fields
Given I am an Admin with Manage Settings permission, When I open Settings > Signature Enrich > Field Mapping, Then I can map each source field (signature.*, enrichment.*) to any standard or custom contact field via typed dropdowns with search. Given I map at least one source to a target, When I save, Then the mapping persists and is versioned with timestamp, user, and diff in the audit log. Given I try to save a mapping where a target has multiple sources and no precedence rule exists for that target, When I save, Then the save is blocked and an inline error lists the targets requiring precedence configuration. Given I map a source to a target with incompatible data type (e.g., array -> string), When I save, Then I receive a blocking validation error naming the offending pair.
Field Precedence Enforcement (Manual > Verified Enrichment > Signature)
Given the default precedence is Manual > Verified Enrichment > Signature and per-field overrides are allowed, When I configure precedence for Job Title to Verified Enrichment > Signature, Then the override is saved and shown in the mapping summary. Given a contact’s Company was set manually, When signature-derived Company arrives, Then the Company field is not updated; the attempt is logged as Skipped with reason "lower precedence". Given a contact’s Phone is sourced from Signature, When a verified enrichment phone arrives, Then the Phone updates to the enrichment value and the source metadata records "verified enrichment", with an audit entry. Given two sources of the same precedence propose different values for the same field in a single run, When processing completes, Then the update is skipped and a conflict event is logged with both candidate values.
Field Locking from Auto-Updates
Given an admin toggles Lock for a target field (e.g., Company), When any non-manual source proposes an update, Then the update is rejected and the field remains unchanged; an audit entry includes source, attempted value, and reason "locked". Given the field is locked, When a human user edits the field manually in the contact profile, Then the manual change is allowed and the source metadata becomes "manual". Given the field is locked and a test run is executed, When viewing the preview, Then the field is highlighted as Locked and shows the proposed-but-suppressed value.
Normalization Rules (Phone E.164, Title Casing, Timezone Canonicalization)
Given Phone normalization is set to E.164 with country fallback from contact locale, When a phone like "(415) 555-1212" and locale "US" is ingested, Then it is stored as "+14155551212"; extensions like "x123" are stored in a separate extension field if mapped. Given Name normalization is set to Title Case, When "joHN van buren" is ingested, Then it is stored as "John van Buren" using exception list for particles. Given Timezone normalization is enabled, When an offset-only value "-0700" or label "PST" is ingested, Then it is converted to a canonical IANA zone "America/Los_Angeles". Given an input fails normalization (e.g., invalid phone), When processing completes, Then the target field is left unchanged and an error is recorded with field, input, and rule.
Per-Field Update Frequency and TTL Controls
Given Update Frequency for Job Title is set to 30 days, When additional updates for Job Title from the same or lower precedence source arrive within 30 days of the last accepted update, Then they are suppressed and logged as "rate-limited". Given TTL for Signature-derived values of Timezone is set to 14 days, When the TTL expires, Then the next qualifying signature value may overwrite older signature-sourced values subject to precedence and locking. Given Update Frequency is set to 0 (no limit) and TTL is set to 0 (no cache), When repeated messages arrive, Then each run evaluates updates without suppression other than precedence/lock rules.
Sample Run and Preview Tool
Given I paste a sample email signature and enrichment JSON and select an existing contact, When I click Run Preview, Then the UI shows per-field Before, Proposed, Normalized value, Source, and Resolution (Applied, Skipped-Locked, Skipped-Precedence, Skipped-Rate-Limited) with no writes performed. Given the preview results, When I click Export, Then a JSON and CSV of the preview rows is downloadable with a unique run ID and timestamp. Given the preview results are satisfactory, When I click Apply Changes, Then only the selected changes are persisted to the chosen contact(s), and an audit entry references the run ID.
Propagation to Billing, Reminders, and Compliance
Given a mapped and normalized Timezone or Locale is updated for a contact, When a reminder is generated after the update, Then the reminder uses the new timezone/locale and is queued within 60 seconds of the contact update. Given Billing relies on Company and Phone fields, When those fields are updated by the mapping engine, Then the next session-to-invoice operation reflects the new values without manual edits; a smoke test generates an invoice draft verifying the values. Given Compliance templates depend on Locale, When Locale changes, Then the appropriate compliance template is selected for subsequent communications; an event is logged showing the template change.

Auto-Form Picker

Chooses and attaches the right intake forms, NDAs, and policy snippets based on keywords, service type, or client segment detected in the thread. Ensures every booking includes what you need for a smooth, compliant first session—without hunting for templates.

Requirements

Conversation & Booking Context Extraction
"As a solo practitioner, I want SoloPilot to infer the service type and client segment from my booking and client messages so that the right forms are picked without manual tagging."
Description

Implement lightweight NLP and deterministic matchers to extract service type, keywords, and client segment from the booking thread, calendar event, and client profile. Normalize detected attributes (e.g., service=“career coaching”, segment=“new client”, keywords=“NDA”, locale=“en-US”) and expose them to downstream selection logic. Ensure privacy-safe processing, multi-language keyword lists, and graceful fallbacks when data is incomplete.

Acceptance Criteria
Cross-Source Attribute Extraction and Normalization
Given a booking thread mentioning "career coaching" and "NDA", a calendar event titled "Intro Career Coaching", and a client profile marked "New Client" When the extractor runs Then it outputs a normalized context with service="career_coaching", segment="new_client", keywords=["nda"], locale="en-US" And all values conform to canonical slugs defined in taxonomy v1.0 (snake_case, lowercase) And duplicates and case variations are deduplicated And the output includes source_attribution per field (thread|event|profile) and a confidence score per field between 0 and 1 And the context object includes extraction_timestamp (ISO-8601 UTC)
Privacy-Safe Processing and Data Minimization
Given context extraction runs for a booking When application logs are inspected Then no raw message content or PII (emails, phone numbers, physical addresses, DOB) is present; only hashed IDs and redacted snippets (e.g., "[REDACTED]") And no new persistent storage of raw thread text is created; transient buffers are cleared within 60 seconds of completion And data in transit uses TLS 1.2+ and logs at rest are encrypted with AES-256 When the account-level setting context_extraction.enabled=false is applied Then the extractor does not run and downstream receives reason="disabled_by_admin" with empty keywords and no service inference
Multi-Language Keyword Detection (en-US and es-ES)
Given client locale=es-ES and thread text "Necesito terapia y firmar un acuerdo de confidencialidad" When extraction runs Then keywords include ["nda"] and service="therapy" is detected using the Spanish keyword list And normalized outputs remain in canonical English slugs while locale="es-ES" is preserved in the context Given locale is missing and content contains Spanish terms When language detection confidence >= 0.7 Then the es-ES keyword list is used; otherwise fallback to en-US And detection precision >= 90% on the provided multilingual test set for top-10 keywords
Deterministic Matchers Override NLP on Exact Matches
Given the thread contains "Service:Executive Coaching" which maps deterministically to service="executive_coaching" And the NLP model suggests service="career_coaching" with higher confidence When extraction resolves the final value Then deterministic mapping prevails and service="executive_coaching" is output with reason="deterministic_match" And if multiple deterministic matches conflict, priority order is thread > calendar_event > client_profile
Graceful Fallbacks With Default Segment and Service
Given no service can be inferred from thread, event, or profile When extraction runs Then service is set to "general_consultation" with confidence=0.0 and warning_code="service_unknown" And segment defaults to "new_client" if the client has no prior paid invoices; otherwise "existing_client" And keywords is [] and processing status="partial" with no thrown errors And downstream receives a valid context object usable by selection logic
Expose Normalized Context to Auto-Form Selection API
Given extraction completes for booking_id=XYZ When the Auto-Form selector requests context Then the API responds 200 with payload matching schema version "context.v1": {service, segment, keywords[], locale, confidence:{...}, source_attribution:{...}, extraction_timestamp} And a system event "context.ready" is published within 1s of completion containing the same payload and an idempotency key so repeated publishes do not create duplicates And when extraction is skipped or partial, the API still returns 200 with reason and defaults applied
Extraction Latency and Resilience
Given average load of 50 requests/min When extraction runs Then p50 latency <= 150ms and p95 latency <= 400ms measured server-side And a hard timeout of 1000ms applies; on timeout, defaults are returned with timeout=true and no exceptions surface to clients And up to 1 retry is attempted on transient errors (HTTP 5xx, timeouts) with exponential backoff starting at 100ms
Form Template Library & Metadata Tagging
"As a practitioner, I want to organize my forms with tags and metadata so that the system can reliably choose the correct template for each booking."
Description

Provide a centralized library to store intake forms, NDAs, and policy snippets with rich metadata (service mappings, client segment, jurisdiction, required/optional, expiry, locale, and version). Support uploads, template variables, previews, and tagging for quick retrieval. Enable soft-delete and restore, and validate templates for missing variables before activation.

Acceptance Criteria
Upload New Template with Type Validation
- Given I am a workspace Admin or Owner in the Template Library, When I upload a supported file (.docx, .pdf, .html, .txt, .md) or create one via the editor, Then the template is saved as Draft with a unique template ID and initial version 1.0.0. - Given I upload an unsupported file type, When I submit the upload, Then the system blocks the upload and displays an error listing the allowed types. - Given the upload succeeds, When I view the template’s details, Then the file name, detected template type, createdBy, and createdAt are recorded and visible.
Metadata Entry and Validation on Save
- Given a Draft template in the editor, When I enter metadata for service mappings, client segment(s), jurisdiction(s), required/optional flag, expiry (date or duration), locale, and version label, Then the system validates required fields and prevents save if any required field is missing, highlighting the field. - Given I save with valid metadata, When I reopen the template, Then the saved metadata values are persisted and retrievable via UI and API. - Given expiry is set to a past date, When I attempt to activate the template, Then activation is blocked with an error that expiry must be in the future.
Template Variable Validation Before Activation
- Given a Draft template containing placeholders in the format {{variable_name}}, When I click Activate, Then the system scans the template and blocks activation if any placeholders are not defined in the workspace variable registry, listing each missing variable. - Given all placeholders are defined, When I click Activate, Then the template status changes to Active and it becomes selectable by the Auto-Form Picker.
Preview Render with Sample Data
- Given a Draft template with placeholders, When I click Preview, Then the system renders a preview with placeholders replaced by sample values or supplied test values without altering the stored template content. - Given a locale is set in metadata, When I preview, Then the preview uses that locale’s formatting for dates and numbers and text direction where applicable. - Given there are unresolved placeholders, When I preview, Then unresolved placeholders are visually highlighted to indicate missing values.
Tagging, Search, and Filter Retrieval
- Given templates exist with metadata and tags, When I search by keyword or filter by service type, client segment, jurisdiction, required/optional, expiry status (active/expired/expiring soon), locale, version status (Active/Draft), or tags, Then results include only templates matching all filters and exclude soft-deleted templates by default. - Given I toggle Include Deleted, When I search, Then soft-deleted templates appear and are clearly marked as Deleted. - Given I change sorting to Updated or Name, When I apply it, Then results reorder accordingly and remain paginated.
Soft-Delete and Restore
- Given I have permission to manage templates, When I soft-delete a template, Then the template moves to a Deleted state with deletedAt and deletedBy recorded and it is no longer selectable by the Auto-Form Picker. - Given a template is soft-deleted, When I restore it, Then it returns to its prior state (Draft or Active) with the same template ID and full version history intact. - Given I choose Permanent Delete on a soft-deleted template, When I confirm, Then the template and all its versions are irreversibly removed and no longer appear in search even when Include Deleted is enabled.
Versioning and Activation Rules
- Given an Active template exists, When I edit it, Then a new Draft version is created (incrementing the version) while the current Active version remains unchanged until I explicitly activate the new version. - Given multiple versions exist for the same template and locale/jurisdiction, When I activate a Draft, Then only one version is Active per locale/jurisdiction and the previously Active version becomes Not Active but remains in version history. - Given a template has an expiry date set in metadata, When the expiry date is reached, Then the template automatically transitions to Expired and is not available for new attachments by the Auto-Form Picker.
Rule-Based Form Selection Engine
"As a practitioner, I want to configure simple rules that map services and client types to forms so that selection is predictable and auditable."
Description

Create a configurable rules engine that maps detected context to required forms. Support condition groups (service, segment, jurisdiction, locale, keywords), precedence, conflict resolution, and default fallbacks. Include a test mode to simulate inputs and preview selected forms. Log rule decisions for troubleshooting and provide safe rollout via draft/publish states.

Acceptance Criteria
Multi-Condition Context Mapping
Given context service_type=SVC_EXEC_COACH, client_segment=SEG_ENTERPRISE, jurisdiction=US-CA, locale=en-US, keywords=["nda","risk"] When the engine evaluates against published rules Then selected_forms == {intake_exec_v2, nda_enterprise, privacy_ca_en, policies_en} and excluded_forms == {} And Then selected_forms has no duplicates and count == 4 And Then evaluation_duration_ms <= 150
Keyword Matching with Thresholds
Given extracted keywords include ["nda", "mutual non-disclosure", "confidential"] When evaluated against keyword rule with synonyms ["nda","non-disclosure","confidentiality"] and match_threshold >= 1 Then form nda_standard is included Given token "nda" appears only as substring in "agenda" When evaluated with whole-word matching enabled Then no keyword match occurs Given keyword appears as "NDA" (mixed case) When evaluated Then match is case-insensitive and form nda_standard is included
Rule Precedence and Exclusive Group Resolution
Given rules R1 precedence=90 adds form nda_standard in exclusive_group=nda and R2 precedence=80 adds form nda_enterprise in exclusive_group=nda When both rules match Then only nda_standard is included, nda_enterprise is excluded, and decision.winner_rule_id == R1 Given rules R3 precedence=70 adds privacy_ca_en (non-exclusive) and R4 precedence=60 adds policies_en (non-exclusive) When both rules match Then both privacy_ca_en and policies_en are included Given two rules in the same exclusive_group tie on precedence When both match Then tie_breaker == most_specific (greater matched conditions) else lowest rule_id wins deterministically
Default Fallback Application
Given no published rules match the input context When evaluated Then selected_forms == default_form_set(workspace) And Then if default_form_set is empty, no forms are attached and a warning log entry is recorded with code=NO_RULES_MATCH severity=warn And Then if any default form is unavailable (archived/missing), it is skipped, an error log entry with code=FORM_NOT_AVAILABLE is recorded, and evaluation succeeds with remaining forms
Draft vs Published Rule Isolation
Given rules R5 state=Draft and R6 state=Published both match When evaluated in live mode Then only R6 contributes to selected_forms Given the same input in test mode with include_drafts=true When evaluated Then R5 and R6 both contribute per precedence and preview is labeled Simulation and no attachments are created Given a rule transitions Draft -> Published When the next evaluation occurs Then the published change takes effect immediately and log.rule_set_version increments by 1
Test Mode Simulation and Preview Output
Given simulated inputs are provided When evaluated in test mode Then the preview displays matched_rule_ids (ordered), selected_forms (ordered by precedence then name), excluded_forms with reasons, and a final_decision_summary And Then no changes occur to bookings/clients/attachments and no notifications are sent And Then the preview is exportable as JSON and CSV and includes a reproducibility_token that replays the same decision
Decision Logging, Auditability, and Idempotency
Given any evaluation occurs When it completes Then a decision log entry is stored with fields {decision_id, timestamp_utc, actor(system|user_id), input_context_hash, rule_set_version, evaluated_rules[{id, matched, reason}], selected_forms[{id, source_rule_id}], excluded_forms[{id, reason}], duration_ms, warnings[], errors[]} And Then logs are queryable by booking_id, client_id, time_range, and rule_id via UI and API And Then repeated evaluations with identical inputs and unchanged published rules produce identical selected_forms order; set_difference(previous, current) == ∅; decision_id is unique; input_context_hash is identical
Auto-Attach & Update Workflow
"As a practitioner, I want forms to auto-attach at booking and stay in sync on changes so that clients always receive the correct paperwork without my intervention."
Description

Automatically attach selected forms to booking confirmations, client portal, and reminders at time of scheduling. Re-evaluate and update attachments on reschedule, service changes, or new context detected. Prevent duplicates, honor required/optional flags, and track per-form completion status. Integrate with notification and reminder systems to nudge clients until completion or deadline.

Acceptance Criteria
Initial Scheduling — Auto-Attach Required Forms
Given a new booking is created for a service with form-mapping rules When the booking is confirmed Then the system attaches all mapped required and optional forms to the booking confirmation email, client portal, and initial reminder And does not attach any form more than once across channels And records an association between the booking, client, form IDs, required/optional flag, and due date And completes within 2 seconds from booking confirmation event And writes an audit log entry with timestamp, rule source, and list of attached form IDs
Reschedule — Re-Evaluate Attachments Without Duplicates
Given an existing booking with attached forms is rescheduled to a different time without service change When the reschedule event is saved Then all previously pending forms remain attached with preserved completion status And reminders are rescheduled to the new timeline relative to the new start time And no duplicate attachments are created And an audit log entry notes reschedule and updated reminder schedule
Service Change — Swap Forms and Preserve Completed Ones
Given an existing booking changes to a different service with a different mapped form set When the change is saved Then forms no longer applicable and not yet completed are detached and marked "removed due to service change" in history And completed forms remain associated and are not requested again And new required and optional forms for the new service are attached And the client is notified of any new or removed form requests And no duplicate attachments are created And the update completes within 3 seconds
New Context Detected in Thread — Update Form Attachments
Given the system detects new keywords or context in the booking conversation that map to additional forms (e.g., NDA) And the context classifier confidence is at or above 0.8 When the context signal is processed Then the corresponding forms are attached to the booking and client portal And the client receives a notification explaining the new request And existing completed forms are not invalidated And duplicates are not created And an audit log records the context trigger, confidence score, and attached form IDs
Completion Tracking and Automated Nudges Until Deadline
Given a form is attached with a due date prior to session start When the form remains incomplete Then the system sends reminders at configured intervals (e.g., 72h, 24h, 2h) until completion or due date And stops sending reminders immediately upon completion And marks the form status as Complete, In Progress, Overdue, or Not Started appropriately And notifies the provider if any required form is Overdue 2 hours before session start And all reminder deliveries are logged with timestamp and channel
Cancellation — Withdraw Pending Forms and Retain Completed Records
Given a booking with attached forms is canceled When the cancellation is saved Then all pending form reminders are canceled And incomplete optional forms are detached from the client portal And incomplete required forms are marked "no longer required due to cancellation" without affecting client compliance metrics And completed forms remain accessible in the client record And an audit log entry records the detachment and reminder cancellations
Consent Capture, Versioning & Audit Trail
"As a practitioner, I want signed forms and their versions stored with an audit trail so that I can prove consent and remain compliant."
Description

Capture signatures and acknowledgments, persist the exact template version sent, timestamps, signer identity signals, and IP/device metadata. Store immutable audit records linked to the booking and client profile, with configurable retention and export for compliance or disputes. Surface status in-session so the practitioner sees whether prerequisites are met before starting.

Acceptance Criteria
Template Version Lock on Consent Submission
Given Auto-Form Picker has sent an intake/consent form for booking X When the client completes and signs the form Then the system stores template_id and template_version used at send time And persists a read-only copy of the rendered document with a SHA-256 checksum And associates the document, version, and checksum to client_id and booking_id And any attempt to alter the stored document or version is rejected and logged
Timestamps and Timezone Normalization
Given server time is synchronized within ±1s of NTP When a consent is sent and later signed Then sent_at and signed_at are recorded in ISO 8601 UTC with millisecond precision And practitioner UI displays times in their configured timezone And clock_skew_ms is recorded if client device time differs by >2s
Signer Identity Signals Captured
Given a client signs via web or mobile When the signature is submitted Then the system records ip_address (IPv4/IPv6), user_agent, device_fingerprint, geo_country/region at sign time And, if authenticated, records user_id and email_verified=true/false And includes these identity signals in the immutable audit record for the consent
Audit Trail Linking and Immutability
Given a booking exists and consent artifacts are generated When viewing the booking or client timeline Then audit events show created_by, sent_at, viewed_at, signed_at, status changes with actor and timestamp And events are append-only and tamper-evident via event_hash and prev_hash And any modification/deletion attempt of past events is blocked and recorded as a security event
In-Session Prerequisite Status Surface
Given I open the live session view for booking X When the service has required consents Then the header shows a status pill per required form: Pending, Viewed, Signed And Start Session is disabled if any required consent is Pending and the workspace setting "block without consent" is enabled And clicking a pill opens consent details including signer identity signals and timestamps
Configurable Retention and Legal Hold
Given workspace consent retention is set to N years When a consent record reaches its retention date Then the system purges PII and stored documents within 24 hours while preserving aggregate counts And if a legal hold exists on the client or booking, deletion is skipped and re-evaluated after hold removal And all retention, deletion, and hold actions are recorded in the audit trail
Export for Compliance or Dispute
Given a user with Role = Owner or Compliance requests an export for booking X or client Y within date range D1–D2 When the export job completes Then a ZIP is produced containing signed PDFs, raw JSON audit records, a metadata CSV, and a SHA-256 manifest And the download link requires authentication and expires after 7 days And file checksums in the manifest match stored values and counts equal the audit trail records
Admin Override & Manual Adjustments UI
"As a practitioner, I want to quickly review and override suggested forms so that I maintain control when special cases arise."
Description

Provide a pre-send review panel to view suggested forms, add/remove items, toggle required/optional, reorder, and save the adjustments as a reusable rule. Show inline warnings for potential compliance gaps and allow one-time overrides without altering global rules. Ensure fast, keyboard-friendly interactions to minimize friction in busy scheduling flows.

Acceptance Criteria
Pre-Send Review Panel Core Actions
Given a booking with detected forms, when the user opens the Review Panel, then the panel loads within 300ms (P95) and lists all suggested items with type, required/optional state, and source (rule or detection). Given the panel is open, when the user removes an item via keyboard Delete or click, then the item is removed and an Undo toast appears for 5 seconds. Given the panel is open, when the user toggles required/optional via Space or click, then the state updates, the badge changes, and validation reflects the new requirement immediately. Given the panel is open, when the user reorders items via keyboard (Alt+Arrow) or drag-and-drop, then the new order persists and is reflected in the send preview. Given the panel is open, when the user searches templates by keyword and presses Enter, then matching forms/NDAs/policy snippets appear within 150ms (P95) and the highlighted item is added. Given changes are made, when the user clicks Send, then the final attachment list matches the panel state exactly.
Save Adjustments as Reusable Rule
Given customized attachments and available metadata (service type, keywords, client segment), when the user selects Save as Rule and names the rule, then a new rule is created with the current configuration and conditions. Given the new rule is saved, when an eligible booking matches its conditions, then the Auto-Form Picker applies the rule automatically and flags the source as "Rule: <name>" in the panel. Given rule saving, when the user opts Save for future only (default), then no existing global defaults are altered. Given conflicting rules exist, when a conflict occurs, then the user is prompted to resolve priority and the selected priority is persisted. Given audit requirements, when a rule is saved or edited, then createdBy, createdAt, version, and changeNote are recorded and viewable from Rule details.
One-Time Override Without Altering Global Rules
Given the panel shows suggestions derived from rules, when the user enables One-time Override, then changes apply only to the current send and are not persisted to any rule or default. Given an override is used, when the booking is finalized, then no new rule is created and global rule statistics remain unchanged. Given compliance, when an override introduces a Blocking compliance gap, then Send is disabled until the gap is resolved or an allowed exception flow is completed with a required justification note (max 300 characters). Given audit, when an override occurs, then an entry is logged with user, timestamp, items changed, and justification.
Inline Compliance Warnings and Resolution
Given detection rules identify potential gaps, when the panel renders, then warnings appear inline next to affected items with severity (Info/Warning/Blocking) and rationale text. Given a warning is shown, when the user clicks Learn more, then a policy link opens in a new tab. Given a Warning severity, when the user adds the suggested item, then the warning resolves immediately without a full reload. Given a Blocking severity, when the user attempts Send, then Send is prevented and an actionable checklist is displayed; upon satisfying all items, Send is enabled. Given state changes, when items are added/removed/toggled, then warnings recalculate within 100ms.
Keyboard-First Accessibility
Given the panel is focused, when the user navigates via Tab/Shift+Tab, then focus order follows visual order and is trapped within the modal until closed. Given screen reader users, when the panel opens, then it announces title, count of items, and instruction text via ARIA and meets WCAG 2.1 AA for name/role/value with no accessibility violations. Given keyboard operations, when the user presses shortcuts (A=Add, R=Toggle required, D=Remove, Alt+Arrow=Reorder, Ctrl+S=Save as Rule, Enter=Send), then actions execute and a visible shortcut hint is available. Given contrast requirements, when the panel is displayed, then actionable elements meet 4.5:1 contrast and have a minimum 44x44px target size.
Performance and Reliability
Given normal network conditions, when opening the panel, then time-to-interactive is ≤300ms (P95); search results appear ≤150ms (P95); reordering latency is ≤50ms (P95); Send confirmation appears ≤500ms (P95). Given autosave, when any change is made, then the state is autosaved locally within 100ms and recoverable after a soft refresh or crash. Given transient API failures, when save-as-rule or search fails, then a retry with exponential backoff is attempted up to 3 times and the user sees a non-blocking error with a retry option. Given offline mode, when the panel is opened offline, then local operations (reorder, toggle, remove) remain usable and changes sync within 5 seconds after connectivity resumes.

Nudge Sequencer

If there’s no response, sends branded, polite follow-ups with refreshed availability across the same channel (email/SMS/DM) at smart intervals. Converts more inquiries into scheduled sessions while you stay hands-off.

Requirements

Smart Interval Engine
"As a solo practitioner, I want nudges to be sent at intelligent times on the same channel so that recipients are more likely to notice and respond without me managing timing."
Description

Determines follow-up send times using configurable cadences and channel-aware heuristics (email/SMS/DM) that consider recipient timezone, sender business hours, last interaction timestamp, and response likelihood. Supports per-sequence rules (e.g., 1 hour, 1 day, 3 days), throttling, skip-weekend options, and guardrails to prevent over-messaging. Integrates with SoloPilot contact activity and scheduler signals to avoid sending near an active booking flow. Centralizes scheduling logic so other modules can query next-send time. Produces an auditable schedule and allows per-contact snooze/skip.

Acceptance Criteria
Per-Sequence Cadence Scheduling With Timezone and Business Hours
Given a sequence with step offsets [1h, 1d, 3d] and a contact in timezone TZ And sender business hours and channel windows are configured And the channel for the next step is Email When the last interaction timestamp is T Then next_send_at is the earliest time ≥ (T + 1h) that falls within both the recipient local allowed window and sender business hours And the scheduled step_id equals the first pending step in the sequence And next_send_at is in the future and persisted as ISO 8601 UTC with contact_id, sequence_id, step_id, and channel
Channel Windows and Weekend Skip Enforcement
Given configured allowed delivery windows for the channel and a skip_weekends flag And a computed next_send_at that may fall outside these constraints When the engine finalizes the schedule Then if next_send_at is outside the allowed window, it is shifted to the start of the next allowed window in the recipient's local time And if skip_weekends is true and next_send_at falls on Saturday or Sunday, it is shifted to the next Monday at the start of the allowed window And an adjustment reason_code is recorded as outside_channel_window or weekend_skipped as applicable And boundary handling is inclusive of window_start and exclusive of window_end And no sends occur during disallowed periods
Throttling and Over-Messaging Guardrails
Given configured limits max_per_24h, max_per_7d, and min_interval_between_sends across all channels for a contact When scheduling a follow-up would breach any limit Then the send is suppressed and next_send_at is moved to the earliest time that satisfies all limits And reason_code = throttled is recorded with the breached limit And no more than one nudge is scheduled for the contact at the same minute across channels And suppression and reschedule are persisted atomically
Booking-Flow Awareness Suppresses Sends
Given contact activity or scheduler signals within the configured booking_silence_window (booking_link_opened, slot_selected, checkout_started, payment_initiated) When computing next_send_at Then the send is suppressed and rescheduled to the end of the booking_silence_window And reason_code = booking_flow_active is recorded And if a booking is confirmed before next_send_at, the pending send is canceled and marked as superseded_by_booking And no sends occur while booking_flow_active is true
Centralized Next-Send Time API Contract
Given a valid contact_id and sequence_id When the NextSendTime API is queried Then the response includes next_send_at (ISO 8601 UTC), channel, step_id, rule_id, reason_code, and correlates to the persisted schedule And repeated queries without state changes return identical values (idempotent) And if no future send is possible, next_send_at is null and reason_code ∈ {completed, throttled, booking_flow_active, snoozed, skipped, canceled} And validation errors return structured error codes and do not mutate state
Auditable Schedule and Decision Log
Given any schedule create, update, suppress, reschedule, cancel, snooze, or skip action When the action is committed Then an immutable audit entry is recorded with timestamp (UTC), actor (system|user), contact_id, sequence_id, step_id, channel, prior_value, new_value, reason_code, and inputs_snapshot (last_interaction_at, recipient_timezone, business_hours, channel_window, limits) And audit entries are queryable by contact_id, sequence_id, and date range And audit export returns CSV and JSON with the recorded fields exactly as stored
Per-Contact Snooze and Skip Controls
Given a contact with an active sequence When snooze_until = S is applied Then no sends occur before S and next_send_at is recomputed at S respecting cadence, windows, and limits And an audit entry is recorded with reason_code = snoozed When skip_next = true is applied Then the next scheduled step is canceled, the following step is scheduled per cadence, and an audit entry is recorded with reason_code = skipped And UI and API state reflect the current snooze/skip status and next_send_at
Same-Channel Thread Continuity
"As a coach, I want nudges to continue in the existing conversation on the original channel so that they feel natural and not like a new outreach."
Description

Sends all follow-ups over the original inquiry channel and preserves conversation threading. For email, sets proper headers (Message-ID, In-Reply-To, References) to keep messages in the same thread; for SMS, uses the same long code/short code; for supported DMs, reuses the original conversation ID. Handles channel-specific rate limits and message size constraints while rendering content appropriately per channel. Stores and reuses per-contact channel metadata and gracefully handles provider errors with within-channel retries. Ensures recipients perceive nudges as a natural continuation rather than a new outreach.

Acceptance Criteria
Email Threading via Headers
Given an original inquiry email is stored with its Message-ID and Subject When the Nudge Sequencer sends a follow-up via email Then the outgoing email includes In-Reply-To and References headers that contain the original Message-ID And the outgoing email has a unique Message-ID And the Subject preserves the original subject text with an appropriate "Re:" prefix (no new subject lines) And From and Reply-To match the configured mailbox used for the original reply And delivery to seeded Gmail and Outlook test inboxes groups the follow-up inside the same thread/conversation as the original within 2 minutes
SMS Continuity via Same Sender ID
Given the original inquiry and prior nudges to a contact were sent via SMS from a specific long code/short code/alphanumeric sender When subsequent follow-ups are sent via SMS Then all follow-ups to that contact are sent using the same sender ID And delivery receipts for each follow-up confirm the same sender ID used previously And if the sender ID is temporarily unavailable, the send is retried (up to 3 attempts with backoff) rather than switching sender IDs And if after retries the sender ID remains unavailable, the attempt is marked Failed and no alternate sender/channel is used
DM Conversation ID Reuse
Given an original inquiry occurred over a supported DM platform with a stored conversationId/threadId When the Nudge Sequencer sends a follow-up over that DM platform Then the API call targets the same conversationId/threadId And if the conversationId is expired or invalid, the system re-opens or rehydrates the thread within the same platform and prefixes the first message with "Following up on our chat on {date}" for context And no cross-channel fallback is attempted And audit logs record both the prior conversationId and the successfully used conversation/thread identifier
Per-Contact Channel Metadata Persistence
Given a contact has prior outreach metadata (channel type, email message-id, SMS sender ID, DM conversationId) When scheduling and sending a follow-up Then the system reads the contact’s channel metadata with p95 latency ≤ 100 ms And the same metadata is reused consistently across retries so the follow-up remains in-thread And upon successful provider acknowledgment, the follow-up’s provider message identifier is stored and linked to the contact and thread And if required metadata is missing, the send is blocked with an actionable error (no message is sent) And metadata persists across service restarts and deployments
Channel-Specific Size and Rendering Constraints
Given channel-specific limits and rendering rules are configured When composing follow-ups Then email messages are sent as multipart/alternative (text/plain + text/html) in UTF-8 and total size ≤ 100 KB And SMS messages exceeding 1 segment are auto-truncated to a maximum of 3 segments (or 306 GSM-7 characters / 201 UCS-2) with an appended short link; content beyond the limit is omitted And DM messages are validated against the platform’s max length and truncated with a short link if necessary And messages that violate size limits are not sent; they are corrected or blocked with clear reasons logged
Provider Error Handling and Within-Channel Retries
Given a transient provider error occurs (HTTP 5xx, timeout, or explicit retryable code) When sending a follow-up on a channel Then the system retries within the same channel using jittered exponential backoff (min 2s, max 5m) up to 3 attempts And an idempotency key ensures no duplicate messages are produced by retries And on permanent errors (HTTP 4xx non-rate-limit), retries stop and the attempt is marked Failed with surfaced error details And all attempts and outcomes are recorded in the send log
Channel Rate Limit Compliance and Backoff
Given provider and workspace rate limits are configured per channel When the Nudge Sequencer sends follow-ups at scale Then per-sender/per-workspace token buckets ensure sends do not exceed configured limits (e.g., 1 SMS/sec per long code; provider-specific defaults) And if a rate-limit response is encountered, messages are queued and retried with backoff within the same channel rather than dropped or cross-channeled And staging load tests of ≥ 1,000 messages complete with zero provider 429/RateLimitExceeded errors and ≥ 95% throughput utilization of configured limits
Branded Templates & Personalization
"As a consultant, I want my follow-ups to match my brand and include personalized details so that nudges feel professional and increase conversions."
Description

Provides a template editor to create polite, on-brand follow-up messages with voice presets, signatures, and assets. Supports merge fields (e.g., first name, service, last message snippet, last contact date) with validation and safe fallbacks. Enables per-channel variants (email/SMS/DM) and previews with test sends. Includes a library of recommended templates tuned for conversion, with the option to A/B test subject lines and phrasing. Automatically injects contextual details such as booking links and session info while maintaining brand consistency.

Acceptance Criteria
Template Creation with Voice Preset and Signature
Given I am in the Template Editor with at least one voice preset and signature configured, When I create a new template with a unique name, select a voice preset and a signature, and click Save, Then the template saves successfully with the selected preset and signature and is retrievable on reload. Given I attempt to save a template without a name, When I click Save, Then a validation error "Template name is required" is shown and the template is not saved. Given I edit the template name and signature, When I click Save, Then the changes are persisted and reflected in previews. Given I duplicate an existing template, When I save the duplicate, Then a new template with a unique ID and a "Copy" suffix in the name is created.
Merge Fields Validation and Safe Fallbacks
Given the editor contains merge fields {{first_name}}, {{service}}, {{last_message_snippet}}, and {{last_contact_date}}, When I click Validate, Then recognized fields are marked valid and any unknown placeholders are highlighted with an error. Given the template contains an unknown merge field {{foo}}, When I attempt to save, Then save is blocked with an error listing the unknown fields. Given the recipient record lacks first_name, When I preview or test-send a template with {{first_name|fallback="there"}}, Then the rendered message shows "there" and no raw placeholder. Given the recipient record lacks last_message_snippet, When I preview or test-send without an explicit fallback, Then the system uses the safe default "(previous message unavailable)" and no raw placeholder appears. Given I insert a merge field from the picker, When I select "Service", Then the placeholder {{service}} is inserted at the cursor.
Per-Channel Variants (Email/SMS/DM) with Channel Rules
Given I open a template, When I add an Email variant with Subject and Body, an SMS variant with Body only, and a DM variant with Body only, Then the template saves with all three channel variants under a single template ID. Given the SMS variant exceeds 320 GSM-7 characters, When I attempt to save, Then I am warned that the message will send as multiple segments and I can still save. Given the SMS or DM variant includes HTML tags, When I validate, Then an error is shown and save is blocked until tags are removed. Given the Email variant lacks a Subject, When I attempt to save, Then save is blocked with "Subject is required for email". Given I switch channels in the editor, When I preview each variant, Then channel-specific formatting and limits are applied and displayed.
Previews and Test Sends per Channel
Given I have a template with channel variants, When I click Preview for Email/SMS/DM, Then the preview renders the selected channel with merge fields resolved against a chosen contact. Given I trigger a Test Send to my own contact details for each channel, When the send completes, Then a success toast with timestamp is shown and an entry is logged in the template's test history. Given the test contact is missing a required delivery endpoint (e.g., no mobile for SMS), When I click Test Send for SMS, Then the action is blocked with a clear error. Given my email branding assets are configured, When I preview an email, Then the brand logo, colors, and signature render according to my brand settings.
Recommended Library Templates and A/B Testing
Given I open the Template Library, When I insert a recommended "Polite Follow-up" template into my workspace, Then the template loads into the editor with placeholders intact and can be saved without errors. Given I enable A/B testing for an email template, When I create Subject A and Subject B and set a 50/50 split, Then both subjects are saved and appear in preview toggles. Given A/B is enabled, When the sequencer sends follow-ups using this template, Then each send is assigned either variant A or B and the assignment is recorded for reporting. Given A/B is disabled, When I save the template, Then only a single subject/body is retained and used.
Automatic Injection of Booking Link and Session Context with Brand Consistency
Given the contact is associated to a service with an active booking link, When I preview or test-send any channel, Then the booking link is automatically injected at the designated placeholder or appended to the footer if no placeholder exists. Given no booking link is available for the selected service, When I preview or test-send, Then the system uses the account default booking link; if none exists, a warning is displayed and no broken link appears. Given the session date/time and service name are available, When I preview or test-send, Then those details are injected into {{service}} and {{session_datetime}} using the account's locale and time zone. Given brand colors, logo, and signature are configured, When the system injects contextual details, Then brand styling remains consistent across email previews and does not alter SMS/DM beyond plain-text content.
Availability Sync at Send-Time
"As a therapist, I want each follow-up to include my current openings so that clients can book immediately without back-and-forth."
Description

Fetches real-time availability from SoloPilot Scheduler at the exact send moment to include accurate openings and a one-click booking link. Selects time slots that match the intended service duration, recipient timezone, and provider preferences, inserting top options inline (e.g., next 3 slots) and updating copy if slots shift. Handles slot expiration and calendar updates between scheduling and sending, falling back to a general booking link or waitlist when no times are available. Shortens links per channel and tracks click-through for conversion analytics.

Acceptance Criteria
Real-Time Availability Fetch at Send-Time
Given a nudge is due to send for a specific service and provider When the message enters the dispatch phase (≤2 seconds before send time) Then the system requests current availability from SoloPilot Scheduler using serviceId, providerId, recipientTimezone, and serviceDuration And selects the next 3 earliest bookable slots strictly after the current time And inserts those slots inline with human-readable times and one-click booking links per slot in the outbound message
Recipient Timezone Alignment and Display
Given the recipient has a stored timezone When formatting inline time options Then the times are converted to the recipient’s timezone and include a timezone abbreviation (e.g., PT) And if the recipient timezone is unknown, fallback to the provider’s timezone and label it accordingly And the displayed times account for any DST changes on the slot date
Service Duration and Provider Preferences Compliance
Given a service has a defined duration and the provider has buffer, working hours, blackout dates, external calendar sync, and daily cap preferences When retrieving and filtering availability Then every returned slot satisfies the service duration plus required buffers And occurs within provider working hours and not on blackout dates And does not overlap external calendar busy times And does not exceed the provider’s daily session cap
Slot Changes Between Queueing and Send-Time
Given candidate slots may have changed since the nudge was scheduled When building the message at send-time Then the system regenerates the inline options from current availability And replaces any stale slots with the latest next up to 3 eligible slots And updates the message copy to reflect the final slots actually sent And if fewer than 3 slots are available, only the available number are shown
No Availability Fallback to Booking or Waitlist
Given no eligible slots are returned for the provider’s booking horizon When generating the message at send-time Then the system omits inline time options And includes a single prominent general booking link And if a waitlist is enabled for the service, includes a waitlist link instead of the general booking link And the message copy updates to invite booking via the provided link
Channel-Specific Link Shortening and Click Tracking
Given the outbound channel is email, SMS, or DM When generating booking URLs (inline options and fallback) Then the system shortens links using the configured shortener for that channel And appends unique tracking parameters (messageId, recipientId, channel, serviceId, slotId when applicable) And records a click-through analytics event upon link resolution with the associated metadata And shortened links resolve to the correct deep link destination
Resilience to Scheduler Errors at Send-Time
Given the SoloPilot Scheduler API is unavailable, returns 4xx/5xx, or times out When attempting to fetch availability at send-time Then the system retries up to 2 times with exponential backoff within 1 second total And if still unsuccessful, sends the message without inline slots and includes the general booking link And logs the error with correlation IDs and marks the message with a recoverable degrade flag And no duplicate sends are triggered by the retry logic
Reply/Booking Detection & Auto-Stop
"As a freelancer, I want sequences to stop the moment someone replies or books so that I don’t annoy clients and look automated."
Description

Monitors incoming replies across email, SMS, and supported DMs, along with SoloPilot booking events, to immediately pause or stop the sequence and prevent additional nudges. Deduplicates signals (e.g., reply and booking within minutes), updates contact status, and records the outcome with timestamps. Optionally sends a polite confirmation or thank-you message and opens a task for the provider if manual follow-up is needed. Supports manual override, sequence resume, and granular logging for troubleshooting.

Acceptance Criteria
Immediate Pause on Reply (Same Channel)
Given a contact has an active nudge sequence on channel X with at least one pending nudge When a reply is received on the same channel/thread for that contact Then pause the sequence within 15 seconds And cancel all pending nudges for that sequence And prevent any further sends until explicitly resumed And set the contact sequence status to "Replied" And persist detection details (channel, message_id, detected_at UTC)
Stop on Booking Event with Deduplication
Given a contact has an active nudge sequence When a SoloPilot booking is created for that contact (new or rescheduled) Then stop the sequence within 15 seconds And cancel all pending nudges And set the contact sequence status to "Booked" And persist booking details (booking_id, start_time, detected_at UTC) Given a reply and a booking occur within a 10-minute window When outcomes are recorded Then record a single final outcome of "Booked" And mark the reply signal as deduplicated with dedup_reason "superseded_by_booking" And ensure only one confirmation and/or task is triggered
Cross-Channel Reply Detection & Global Stop
Given a contact has active sequences on any supported channels (email, SMS, DM) When a reply is received on any mapped channel for that contact Then stop or pause all active sequences for that contact within 15 seconds And set the originating channel on the outcome to the reply channel And ensure no further nudges are sent across channels until explicitly resumed
Outcome Logging & Timeline Visibility
Given any stop/pause trigger (reply, booking, manual action) When the outcome is recorded Then create an immutable outcome record with fields: contact_id, sequence_id, trigger_type, channel, source_ids (message_id or booking_id), detected_at (UTC ISO 8601), actor_id (system or user), dedup_key And publish the outcome to the contact timeline within 60 seconds And expose the outcome via the Outcomes API endpoint And display a human-readable entry in the UI timeline reflecting the trigger and status change
Post-Stop Actions: Confirmation Message and Task Creation
Given workspace setting "Auto-confirmation on stop" is enabled When a sequence is paused or stopped due to a reply or booking Then send one brand-compliant confirmation/thank-you on the originating channel within 60 seconds And include appointment details for bookings And do not send if a confirmation was sent in the last 24 hours for the same contact and sequence Given a reply is classified as "Needs manual follow-up" by rules or ML When the sequence stops Then create one task assigned to the provider within 30 seconds with link to the contact, reply snippet, and due_at SLA (default 24h) And prevent duplicate tasks for the same conversation within a 24-hour window
Manual Override and Sequence Resume
Given a sequence was auto-paused or auto-stopped When a user with appropriate permissions clicks "Resume sequence" in the UI Then schedule the next pending nudge according to sequence rules within 60 seconds And log a manual action record with actor_id, action="resume", and timestamp (UTC) And update the sequence status to "Active" Given a sequence is active When a user applies an "Ignore replies for N hours" override Then the system will not auto-stop due to replies during the override window And log detection events during the window as "ignored_by_override" without affecting sends
Granular Troubleshooting Logs and Error Handling
Given a detection or stop event is processed When logging is performed Then write a debug log entry including detection_rule_id, channel, message_id or booking_id, thread identifiers, normalization steps taken, dedup decision, and processing latency And retain debug logs for at least 30 days And make logs queryable via an admin-only log viewer with filter by contact_id, sequence_id, and time range Given an external provider error or rate limit occurs during detection or stop actions When retry policy is applied Then retry up to 3 times with exponential backoff And surface a final failure alert in the admin console if retries exhaust And fail safe by preventing further nudges until the issue is resolved or a manual resume is performed
Compliance, Opt-Outs, and Quiet Hours
"As a business owner, I want nudges to follow legal and ethical sending rules so that I protect my brand and avoid penalties."
Description

Enforces per-contact consent and channel-level preferences, honoring opt-out keywords (e.g., STOP) and unsubscribe links. Applies quiet hours by recipient timezone and respects regional regulations (e.g., TCPA for SMS, GDPR data handling), with configurable send windows and suppression lists. Maintains immutable audit logs for sends, replies, and opt-out events, and performs bounce/blocked number handling with automatic suppression. Provides admin controls to configure compliance defaults per workspace.

Acceptance Criteria
Per-Contact Consent Enforcement by Channel
- Given a contact has no recorded consent for SMS and the sequence step targets SMS, When the send is evaluated, Then the SMS is not sent, the event is logged as "blocked - no consent", and any configured channel fallback is attempted within the same evaluation cycle. - Given a contact has consent for email but not SMS, When a multichannel step is evaluated, Then only email is sent and SMS is suppressed. - Given a contact's consent state is updated to "revoked" for a channel, When any pending or future steps are evaluated, Then that channel is suppressed within 60 seconds and suppression is reflected in the contact's preferences. - Given a consent record exists for a contact and channel, When viewed, Then it displays channel, lawful basis (if applicable), timestamp, source, and IP (if collected).
Channel-Specific Opt-Out Processing (SMS STOP and Email Unsubscribe)
- Given an inbound SMS containing STOP, STOPALL, UNSUBSCRIBE, or CANCEL (case-insensitive, trimmed), When received, Then mark the SMS channel opted-out for that contact within 5 seconds, log the event, and suppress all future SMS sends from sequences. - Given a contact clicks a unique unsubscribe link in an email and selects "Unsubscribe from email", When submitted, Then the email channel is suppressed within 60 seconds, a confirmation page is shown, and the event is logged with IP and user agent. - Given a contact previously opted out of SMS, When they send START, UNSTOP, or YES, Then require an explicit confirmation reply "YES" to re-opt-in; upon confirmation, lift suppression and log the re-opt-in with timestamp. - Given an unsubscribe link is accessed with an invalid or expired token, When the link is visited, Then no changes are made and a secure option to request a fresh link is presented and logged.
Quiet Hours by Recipient Timezone
- Given quiet hours are configured as 20:00–08:00 local time for SMS and email, When a sequence step becomes due at 21:30 in the recipient's timezone, Then the send is deferred to the next available window starting 08:00 local time, preserving message order. - Given a contact's timezone is unknown, When evaluating quiet hours, Then the system uses the workspace default timezone and logs the fallback. - Given a message is deferred due to quiet hours, When the send window opens, Then the message is dispatched within 0–10 minutes randomized delay and the deferral is recorded in the audit log. - Given an admin updates quiet hours, When evaluation runs next, Then new windows take effect for all future evaluations within 5 minutes.
Regional Compliance Rules Application (TCPA/GDPR)
- Given an SMS to a US phone number is promotional and prior express written consent is not recorded, When the send is evaluated, Then the SMS is blocked, logged as "blocked - TCPA consent missing", and the sequence proceeds per fallback rules without sending SMS. - Given a contact is flagged as EU/UK resident, When processing their data for messaging, Then a lawful basis is required and stored in the consent record before any sends are allowed. - Given an admin executes a GDPR erasure request on a contact, When processed, Then PII is deleted from operational systems, audit logs retain immutable event metadata with pseudonymized identifiers, and a completion receipt is logged within 72 hours. - Given regional configuration is ambiguous, When evaluating compliance, Then the stricter rule applies by default and the decision is logged.
Bounce and Blocked Number Handling with Auto-Suppression
- Given an email returns a hard bounce from the provider, When the bounce webhook is received, Then the email channel for that contact is immediately suppressed, the bounce reason and code are logged, and no further emails are sent from sequences. - Given an SMS provider returns "unknown subscriber" or "blocked" for a send attempt, When the delivery receipt is received, Then the SMS channel is suppressed for that contact and the error code is logged. - Given a soft bounce (e.g., mailbox full or temporary failure) is received, When retry policy is applied, Then up to 3 retries occur over 24 hours with exponential backoff; after final failure, the channel is paused for 24 hours and the event is logged. - Given a suppressed channel is manually reactivated by an admin, When reactivation occurs, Then the action requires an entered reason, is logged, and previously suppressed messages are not retroactively sent.
Immutable Audit Logs for Messaging and Compliance Events
- Given any outbound message, inbound reply, opt-in/out change, suppression, consent change, or compliance block, When the event occurs, Then an audit log entry is appended with UTC timestamp, contact id, channel, actor (system/user), provider ids, region, and decision. - Given audit logs, When a user attempts to edit or delete an entry, Then the action is denied, an error is shown, and the attempt is itself logged. - Given new logs are written, When persisted, Then entries are tamper-evident via cryptographic hash chaining across records and verifiable via an integrity check endpoint. - Given an admin queries logs, When filters (contact, channel, date range, event type) are applied, Then results return within 5 seconds for up to 10,000 records and can be exported as CSV or JSON subject to role-based access controls.
Admin Controls for Compliance Defaults per Workspace
- Given a workspace admin, When accessing Compliance Settings, Then they can configure per-channel consent requirements, quiet hour windows, unsubscribe footer content, opt-out keyword behavior, retry policies, and regional rule strictness. - Given settings are updated, When saved, Then changes apply to all future evaluations within 5 minutes and are versioned with who/what/when in audit logs. - Given a new workspace is created, When defaults are not customized, Then safe defaults are applied: email unsubscribe required, SMS TCPA strict mode, quiet hours 20:00–08:00, and strict regional fallback. - Given role-based access controls, When a non-admin attempts to change compliance settings, Then access is denied and the attempt is logged.
Sequence Builder & Performance Analytics
"As a solo operator, I want to design sequences and see what works so that I can improve conversions over time."
Description

Offers a visual builder to create, edit, clone, and assign nudge sequences to inquiry sources or pipelines. Configures per-step timing, content, and stop rules with preview of channel-specific rendering. Provides analytics by sequence and step, including send/deliver/open/click/reply/book rates and time-to-book, with filters by channel, service, and cohort. Supports exporting metrics, surfacing optimization suggestions (e.g., adjust step-2 delay), and A/B testing for copy and timing to iteratively improve conversion.

Acceptance Criteria
Visual Builder: Create, Edit, Clone, Assign Sequences
Given I have editor permissions, When I open the Nudge Sequencer visual builder and create a new sequence, Then I can add, reorder, and delete steps via drag-and-drop. Given I configure a step, When I set timing, Then I can choose delay units (minutes/hours/days/weeks) relative to trigger or previous step and optionally restrict to business hours. Given I edit step content, When I select a channel (email/SMS/DM), Then channel-specific editors and variables are available and validation enforces channel constraints. Given I define stop rules, When a reply is detected or a booking is created, Then the sequence stops for that contact and the stop reason is logged. Given I save the sequence, When I publish it, Then a new immutable version is created and the status changes from Draft to Active. Given an existing sequence, When I clone it, Then all steps and settings are duplicated and performance data is excluded. Given pipelines or inquiry sources exist, When I assign the sequence to one, Then new inquiries from that source/pipeline auto-enroll in the Active version. Given I preview the sequence, When I choose a sample contact, Then variables resolve with sample data and channel formatting is shown.
Channel-Specific Rendering Preview
Given I select Email preview, When I toggle Desktop/Mobile, Then layout, subject, preheader, link tracking, and branding render as sent. Given I select SMS preview, When content exceeds one segment, Then the system shows estimated segments and carrier-safe character count. Given I select DM preview, When platform limitations apply (e.g., no clickable links), Then the preview reflects those constraints and displays a non-blocking warning. Given I insert dynamic variables, When a variable may be missing, Then the preview shows fallback values and validation blocks publish until a fallback is provided. Given I send a test message, When I specify a test recipient, Then the message delivers via the selected channel and is logged in test activity with timestamp and status.
Stop Rules and Auto-Halt on Reply/Booking
Given a sequence with stop rules enabled, When a recipient replies via any enrolled channel, Then all remaining steps are canceled for that recipient within 1 minute and the stop reason is recorded as Reply. Given a booking event is created for the recipient that matches the targeted service, When the event is detected, Then the sequence cancels and logs Booked as the stop reason. Given a step hard-bounces or fails on a channel, When a fallback channel is configured, Then the next step sends via the fallback channel and the failure is logged. Given a per-step stop rule is set to stop on link click, When the recipient clicks the tracked link, Then downstream steps are canceled. Given a manual pause is applied to a contact, When the sequence is paused, Then no steps send during the pause window and the schedule resumes after the pause ends.
Analytics by Sequence and Step with Filters
Given at least 30 days of activity exist, When I open Analytics, Then I can switch between By Sequence and By Step views. Given the analytics view, When I apply filters for channel, service, cohort, and date range, Then counts and rates recalculate within 2 seconds and show sample size n. Given metrics display, Then each sequence/step shows sends, delivered, open rate (opens/delivered), click rate (clicks/delivered), reply rate (replies/delivered), book rate (bookings/unique recipients), and median time-to-book (with 95% CI). Given I click a metric, When I drill down, Then I see contributing records with timestamps, channel, and identifiers. Given workspace time zone is adjusted, When I change time zone, Then analytics rebase to the selected time zone consistently across UI and exports.
Metrics Export with Date Range and Filters
Given filters and a date range are selected, When I export CSV, Then the file includes one row per sequence/step with columns: sequence_id, sequence_name, version, step_number, channel, sends, delivered, opens, clicks, replies, bookings, open_rate, click_rate, reply_rate, book_rate, ttb_median_seconds, ttb_mean_seconds, cohort, service, start_date, end_date, timezone. Given the export completes, Then row counts and metric totals match the UI within ±0.1% and the file is UTF-8 with headers. Given the dataset exceeds 100k rows, When I request export, Then I receive a downloadable link via email that expires in 7 days and the export event is audit-logged with user, timestamp, filters, and file size. Given I re-run an export with identical filters and window, When no new data has arrived, Then a cached export is delivered and labeled as Cached with its generation timestamp.
Optimization Suggestions Surface and Actions
Given a step has ≥200 deliveries in the last 30 days, When its metric underperforms the workspace benchmark by ≥15%, Then the system surfaces a suggestion with rationale and a recommended change (e.g., increase delay from 12h to 24h). Given a suggestion is shown, When I click Apply, Then I am taken to the editor with the proposed value prefilled and a draft version is created without impacting the Active version until published. Given I dismiss a suggestion, Then it is hidden for 30 days or until the metric improves/worsens by ≥10%, whichever occurs first. Given a suggestion is accepted and published, When sufficient post-change data is collected (e.g., 14 days or 200 deliveries), Then the system displays measured impact versus baseline with uplift and confidence. Given multiple suggestions qualify, Then no more than 3 active suggestions are shown per sequence and they are prioritized by expected impact.
A/B Testing for Step Copy and Timing
Given a step supports variants, When I create variants A and B for copy and/or timing, Then I can set traffic split (10–90 granularity) and minimum sample size per arm. Given the test runs, When recipients are enrolled, Then each recipient is randomly assigned to a variant and remains in that variant for all related messages. Given reporting is available, When minimum sample size and time window are met (e.g., 200 deliveries per arm and 7 days), Then the UI shows per-variant conversion to booking, lift, and statistical confidence, and highlights a leader if confidence ≥95%. Given I declare a winner, When I click Roll Out, Then the winner is set to 100% traffic, the loser is archived, and historical data is retained. Given Auto-Winner is enabled, When significance ≥95% after minimum sample is reached, Then the system auto-rolls out the winner and notifies me.

Thread Sync

Links the original conversation to the booking, notes, and invoice with a clean timeline of messages, offers, and confirmations. Gives you a single place to review context, audit decisions, and send updates or reschedule links without tool-switching.

Requirements

Email & SMS Thread Connectors
"As a solo practitioner, I want SoloPilot to automatically sync my email and SMS conversations so that all session context and client communications appear in one place without manual copy‑paste."
Description

Provide first‑class connectors for Gmail/Google Workspace and Microsoft 365 (Graph API) plus an SMS gateway (e.g., Twilio) to ingest and send messages while preserving native threading. Normalize emails/SMS into a common message schema including headers, participants, timestamps, and delivery status, and store minimal content needed for context. Support OAuth, token refresh, rate limiting, retries with exponential backoff, idempotent ingestion, and deduplication via Message‑ID/In‑Reply‑To/References and SMS conversation identifiers. Map messages to SoloPilot contacts and surface them in bookings, notes, and invoices by thread ID. Enable two‑way sync so messages sent from SoloPilot appear in the user’s mailbox/SMS history with proper sender identity, signatures, and reply‑to routing. Integrate with workspace settings for provider connection, per‑channel enablement, and health diagnostics.

Acceptance Criteria
OAuth Provider Connection & Health Diagnostics
Given a workspace admin opens Settings > Connectors, When they connect Gmail or Microsoft 365 via OAuth with least-privilege scopes, Then access and refresh tokens are stored encrypted-at-rest, scopes are recorded, and Health shows Connected with provider name, granted scopes, and last sync timestamp. Given the access token expires, When background sync triggers, Then token refresh succeeds without user intervention and Health remains Connected. Given token refresh fails (e.g., invalid_grant) or consent is revoked, When the next sync attempt occurs, Then connector status changes to Action Required with provider error code/message and a Reconnect action, And ingestion/sending are suspended until resolved. Given per-channel enablement toggles, When the admin disables Email or SMS, Then new ingestion and outbound for that channel stop within 60 seconds, existing timelines remain visible, and the toggle change is audit-logged with actor, time, and previous/new values.
Ingest & Normalize Emails/SMS into Common Schema
Given an inbound email from Gmail/Graph or SMS from Twilio is received via webhook or poll, When the message is processed, Then a message record is created using the common schema with fields: provider, direction, thread_id, message_id, in_reply_to, references, participants (from/to/cc/bcc or phone), subject (email only), timestamp (UTC ISO 8601), body_snippet ≤ 500 chars, attachment metadata (name,size,MIME,hash), and delivery_status initial value. Given data minimization rules, When storing content, Then only headers, participants, and body_snippet are stored; full body content is not persisted; attachments are not stored, only metadata and secure fetch URL if applicable. Given PII redaction rules, When generating body_snippet, Then credit card numbers and SSNs are masked to policy, and redaction is verifiable in stored snippet.
Deduplicate & Preserve Native Threading
Given the same email arrives via both push and poll or an SMS webhook is retried, When ingestion runs, Then processing is idempotent and duplicates are discarded using Message-ID/In-Reply-To/References for email and Conversation SID + Message SID for SMS, producing at most one stored record and one timeline entry. Given an email without a Message-ID, When ingesting, Then a deterministic surrogate ID (hash of normalized headers + date + provider id) is generated and used for deduplication. Given replies and forwards, When assigning thread_id, Then native threading is preserved using References/In-Reply-To for email and Conversation SID for SMS so that messages appear in a single timeline in chronological order. Given provider events are out-of-order, When ordering timeline, Then final ordering uses provider timestamp and a stable tiebreaker so the timeline matches the provider thread view.
Map Messages to Contacts and Surface by Thread ID
Given a message's participants match existing SoloPilot contacts by verified email or phone, When ingestion completes, Then the message is linked to those contacts and indexed by thread_id. Given a user opens a booking, note, or invoice linked to the same contact(s) and thread_id, When viewing the Thread Sync panel, Then the last 20 messages render within 300 ms p95 with channel filters (Email/SMS) and message direction indicators. Given no matching contact exists, When ingestion detects a new participant, Then a suggested contact match is displayed with Add/Link actions; upon user confirmation, the timeline reindexes to include the message within 5 seconds. Given multiple contacts share an alias, When ambiguity is detected, Then the system flags the message as Unlinked and requires explicit user selection before linking.
Outbound Send Mirrors to Provider with Identity & Replies
Given a user replies from Thread Sync using a connected Gmail or Microsoft 365 account, When sending, Then the message is sent via the provider API with From/Sender/Reply-To matching the selected workspace identity and existing DKIM/SPF alignment is preserved; the email appears in the provider Sent folder within 10 seconds and remains in the native thread via proper In-Reply-To/References or Graph conversationId. Given an outbound SMS, When sending via Twilio, Then the message is recorded with provider message SID, appears in Twilio logs, and is delivered to the recipient, with delivery status transitioning queued→sent→delivered/failed and provider codes stored. Given a recipient replies to email or SMS, When provider notifies, Then the reply is ingested and appended to the same timeline within 15 seconds p95, preserving threading and participant attribution.
Rate Limits, Retries, Backoff, and Error Reporting
Given provider APIs return 429 or 5xx, When retrying, Then exponential backoff with jitter is used starting at 1s doubling to a max of 5m with a cap of 7 attempts, respecting Retry-After headers when present. Given retries are exhausted, When the operation still fails, Then the item is moved to a dead-letter queue with correlation ID, surfaced in Health Diagnostics with provider code and last attempt time, and a one-click Reprocess action is available. Given sustained high throughput, When hitting provider quotas, Then the system auto-throttles to stay within limits, maintaining p95 API error rate < 1% and no data loss; backfills progress is reported with remaining count and ETA. Given transient network failures, When idempotent operations are retried, Then no duplicate sends or duplicate ingests occur and timeline integrity is maintained.
Auto‑Association Engine
"As a consultant, I want messages to auto‑link to the right session and invoice so that I don’t waste time organizing threads or risk missing billable details."
Description

Automatically associate incoming/outgoing messages with the correct booking, client notes, and invoice using deterministic and heuristic signals: participant matching to contacts, unique tokens embedded in scheduling/reschedule/offer links, subject markers, proximity to session date/time, and invoice numbers. Provide a confidence score with a review queue for low‑confidence matches and allow one‑click manual override and bulk reassignment. Handle late associations when bookings are created after messages arrive, and reindex on entity updates. Prevent duplicates across related entities by enforcing a single source thread with cross‑links. Expose association events to the audit log and surface suggested matches in the UI for quick confirmation.

Acceptance Criteria
Deterministic Association via Unique Tokens and IDs
Given an inbound or outbound message includes a valid SoloPilot scheduling/reschedule/offer link token or an invoice number When the Auto-Association Engine processes the message Then it associates the message to the referenced booking and marks the booking's primary thread as the source And cross-links any existing related notes and invoice to that same thread; if not yet created, it defers linking and schedules a late association And sets confidence_score to 1.0 and method="deterministic" And completes the association within 3 seconds of message ingestion And records an audit event "association.created" with fields: message_id, entity_type, entity_id, method, token_type, token_value, actor="system", timestamp
Heuristic Association by Participant Match and Time Proximity
Given a message lacks deterministic tokens but all participants map to a single SoloPilot contact And that contact has exactly one booking within ±14 days of the message timestamp When the engine processes the message Then it computes a confidence_score using participant match, time proximity to the session, and subject markers And if confidence_score ≥ 0.85, it auto-associates the message to that booking within 5 seconds and logs method="heuristic" And if 0.60 ≤ confidence_score < 0.85, it does not associate but places the message in the Review Queue with the top 3 suggested bookings and reason breakdown And if confidence_score < 0.60 or participants map to multiple contacts, it leaves the message unassociated and visible in the Unmatched list
Confidence Thresholds and Review Queue Workflow
Given a message is routed to the Review Queue When the user opens the queue item Then the UI displays: current confidence_score (0.00–1.00), top 3 candidate entities (booking/notes/invoice), and reason codes (e.g., participant_match, time_proximity, subject_marker) And the user can Confirm any candidate in one click or Dismiss all And on Confirm, the engine associates the message within 1 second, sets method="review_confirmed", and writes an "association.created" audit event with reviewer_id And on Dismiss, the message remains unassociated and an "association.dismissed" audit event is recorded And queue items are created within 2 seconds of message ingestion and sorted by descending confidence_score
One-Click Manual Override and Bulk Reassignment
Given a message is associated to an entity When a user clicks "Associate to…" and selects a different booking/notes/invoice Then the engine reassigns the message in one step, updates cross-links to the booking's primary thread, and records an "association.updated" audit event with method="manual" And the new association is locked (override_lock=true) to prevent future automatic re-assignment unless explicitly unlocked by a user And previous duplicate links (if any) are removed so the message appears once in the timeline And bulk reassignment supports selecting 1–500 messages, completes within 60 seconds, applies the same lock behavior, and records one "association.bulk_updated" parent audit event plus per-message child events
Late Association and Reindex on Entity Updates
Given messages arrive before a booking, notes, or invoice is created for a contact When the entity is created or updated (e.g., new booking; booking date/time change; contact email/phone update) Then the engine re-indexes impacted messages within 10 seconds, recomputes confidence, and links messages meeting auto-association thresholds And manual-override (override_lock=true) associations are never changed by reindex And on booking date/time updates, messages may be moved to a different booking only if the new confidence increases by ≥ 0.15 and is ≥ 0.85, with an "association.moved" audit event capturing before/after entity IDs and scores And late associations respect deterministic tokens (priority over heuristics) when present in historical messages
Single Source Thread and Duplicate Prevention Across Entities
Given a booking has a designated primary thread ID When messages are associated via deterministic or heuristic methods Then notes and invoice entities cross-link to that same primary thread rather than creating separate threads And a message cannot be associated as primary to more than one thread for the same booking (enforced via a unique constraint on message_id + booking_id) And the UI timeline displays each message once per booking, with links to related notes and invoice, and no duplicates even under concurrent updates And attempts to create a second primary thread for a booking are rejected with a clear error and no partial writes
Audit Logging and Suggested Matches in UI
Given any association create/update/delete occurs When the event is written to the audit log Then the record includes: message_id, entity_type, entity_id, action (created|updated|deleted|moved|dismissed), method (deterministic|heuristic|review_confirmed|manual|bulk), confidence_score (if applicable), reason codes, actor_id (system or user), and timestamp And audit events are immediately queryable via the admin audit UI and API And for unassociated or low-confidence messages, the UI surfaces suggested matches (up to 3) with confidence and reason chips, and a one-click Confirm that performs the association and logs the event
Unified Thread Timeline
"As a therapist, I want a single timeline showing all communications and related events for a session so that I can quickly review context and make informed decisions without switching tools."
Description

Render a chronological, consolidated timeline that merges emails, SMS, booking events (scheduled, rescheduled, canceled), offers/acceptances, notes creation/edits, invoice creation/sends/payments, and system automations into a single view. Provide inline previews of message bodies and attachments with quick open/download, visual tags for event types, avatars for actors, and delivery/read status where available. Include filters (event type, date range, participant), full‑text search over message metadata/content (respecting permissions), and deep links to the underlying booking, note, or invoice. Embed the timeline on client profile, booking detail, note, and invoice pages, with responsive design for mobile. Support pagination/virtualization for performance and export to PDF for audits.

Acceptance Criteria
Chronological Consolidation and Event Metadata Display
Given a client thread contains emails, SMS, booking events (scheduled, rescheduled, canceled), offers/acceptances, notes (created/edited), invoices (created/sent/paid), and system automations with timestamps When the Unified Thread Timeline loads Then all events are rendered in a single list sorted by event timestamp ascending with a deterministic tie-breaker on created_at then event_id And each event shows a visual tag for its type, the actor avatar (or system icon), and channel delivery/read status when provided by the source And interleaved events from different sources are shown without duplication and with accurate timestamps localized to the viewer's timezone
Inline Message and Attachment Preview with Quick Actions
Given an email or SMS event with body text and one or more attachments (images, PDFs, other files) in the timeline When the user expands the event preview Then the message body renders with safe formatting and line breaks preserved And supported attachments display inline previews (image thumbnails; first page of PDF); unsupported types show a file-type icon And clicking Open displays the full message or attachment; clicking Download initiates a download using the original filename And if the user lacks permission to view an attachment, the preview is replaced with a locked placeholder and Open/Download are disabled
Filtering by Event Type, Date Range, and Participant
Given the timeline filters for Event Type (multi-select), Date Range, and Participant are available When the user applies any combination of these filters Then only events matching all active filters are displayed in the timeline And active filters are shown as removable chips and a Clear All action resets the view to unfiltered And if no events match, an empty state is shown with an option to clear filters And active filters persist for the user within the same client context during the current session
Full-Text Search with Permission Enforcement
Given the user has permission to view a subset of messages, notes, bookings, and invoices in the thread When the user searches using a text query Then results include only events the user is authorized to view and that match the query in message subject, body, note content, booking title, or invoice number And matching terms are highlighted within the inline preview And search respects any active filters and date range And clearing the query restores the prior filtered timeline
Deep Links to Underlying Records
Given each timeline event exposes a deep link to its source record (booking, note, invoice, email/SMS) When the user activates the deep link Then the app navigates to the correct record view in the same workspace with the record loaded And if the record has been deleted or the user lacks permission, an error state is shown with a safe back-to-timeline option And returning to the timeline restores the prior scroll position and active filters/search
Embedding and Responsive Mobile Layout
Given the timeline component is embedded on Client Profile, Booking Detail, Note, and Invoice pages When each page is viewed on desktop and mobile (>=320px width) Then the timeline renders the same functionality on all host pages with no horizontal scrolling on mobile And interactive targets meet minimum 44px touch size and content reflows to fit the viewport And avatars, tags, and status indicators remain visible and legible at all breakpoints
Pagination/Virtualization Performance and PDF Export
Given a thread with 5000+ events When the user loads the timeline Then the first 50 events render within 2 seconds and additional pages load in increments of 50 within 1 second And virtualized scrolling maintains smooth interaction without visible jank and mounts only visible items to keep memory usage stable And reaching the end of available events displays an end-of-timeline indicator And when the user clicks Export to PDF, the generated PDF reflects the current filters/date range/search, preserves chronological order, includes a header with client name and generation timestamp, numbers pages, and downloads with filename pattern clientname_thread_YYYYMMDD.pdf
Timeline Actions & Templates
"As a coach, I want to send confirmations and reschedule links directly from the thread view so that I can respond faster and keep all communication in one place."
Description

Enable action controls directly from the timeline to compose and send email/SMS updates, reschedule links, offers, confirmations, and payment nudges without leaving the page. Provide a template library with variables (client name, session date/time, location/telehealth link, invoice amount, due date) and preview before send. Allow attaching files from notes or uploads, scheduling messages for later, and inserting one‑time secure links. Respect channel availability and sender identity settings, and log all sends back to the same thread with status updates. Expose quick actions (Confirm, Reschedule, Send Receipt) and keyboard shortcuts to accelerate workflows.

Acceptance Criteria
Compose and Send from Timeline with Template Variables and Preview
Given a user is viewing a client thread timeline linked to an upcoming session and invoice When the user opens the composer, selects a template, and chooses a channel (Email or SMS) Then the template auto-populates variables: client_name, session_datetime (with timezone), location_or_telehealth_link, invoice_amount, due_date from the linked records And any unresolved or missing variable is highlighted and blocks sending with a clear validation message And a preview shows the exact rendered outbound content for the chosen channel When the user edits the body or variable fields Then the preview updates in real time When the user clicks Send Then the message is dispatched over the selected channel without leaving the page and the sent content snapshot is stored with the message
Channel Availability and Sender Identity Enforcement
Given the client record has channel availability settings and the workspace has verified sender identities When the user opens the channel selector in the timeline composer Then unavailable channels are disabled with explanatory tooltips And the default From identity is preselected based on workspace rules for the chosen channel When no verified identity exists for the selected channel Then sending is blocked with a clear error and a link to manage sender identities And the system does not auto-switch channels without explicit user confirmation
Attach Files and Insert One‑Time Secure Links
Given client notes and uploads contain files When the user clicks Attach and selects files from Notes or Uploads Then the files are attached to the outbound message with source labels and can be removed before sending When the user inserts a One‑time secure link to an attachment Then a unique link token is generated and inserted into the message And after the first successful access the link expires and cannot be opened again And the timeline records an access audit event with timestamp and recipient identifier When the message is sent Then attachments and secure link metadata are logged to the same thread
Schedule Send with Edit/Cancel and On‑Time Delivery
Given the user composes a message in the timeline When the user selects Schedule for Later and sets a future date and time Then the message is saved as Scheduled with the chosen timestamp visible in the thread And the user can edit the content, change the scheduled time, or cancel before it is sent At the scheduled time Then the system dispatches the message within 60 seconds of the scheduled timestamp And the timeline updates status from Scheduled to Sent (or Failed) with exact send time When a scheduled message is canceled Then no outbound is dispatched and the thread shows Canceled with audit details
Quick Actions and Keyboard Shortcuts
Given a session in the thread is Pending and an invoice exists When the user clicks the Confirm quick action or presses the shortcut (e.g., C) Then the session status updates to Confirmed, a confirmation message is sent using the default confirmation template, and the event is logged in the thread When the user clicks Reschedule or presses the shortcut (e.g., R) Then a reschedule link is inserted using the booking context, the reschedule message is sent, and the thread records the action When the user clicks Send Receipt or presses the shortcut (e.g., G) after a payment is recorded Then a receipt message is sent to the client and logged with the associated invoice When the user presses ? in the timeline Then a shortcut help overlay appears listing available shortcuts for visible quick actions
Thread Logging and Delivery Status Updates
Given any message is sent or scheduled from the timeline When the action updates status (Queued, Sent, Delivered, Failed) Then an entry is added to the same thread including timestamp, channel, sender identity, template name (if used), and a non-editable snapshot of the content And delivery status updates in place without page reload and records status transitions with times When a send fails Then the thread entry shows Failed with error detail, a Retry action is available, and a subsequent retry attempt is logged as a separate status update
Payment Nudge with Invoice Variables and Pay Link
Given an open or overdue invoice is linked to the thread When the user selects the Payment Nudge action in the composer and chooses a channel Then the message body auto-populates invoice_amount, due_date, and a secure pay_link And the preview shows the full rendered message including the pay_link When the user sends the nudge Then the message is dispatched, the thread logs the send with invoice reference, and the invoice timeline records the nudge event
Audit & Compliance Trail
"As an owner, I want a reliable audit trail of communications and changes so that I can verify decisions and meet client or regulatory requests with confidence."
Description

Maintain an immutable event ledger capturing message ingestion, associations, edits, unlink/relink actions, note revisions, invoice updates, and timeline sends, with user, timestamp, and before/after metadata. Store message metadata and content hashes to prove integrity without exposing sensitive content unnecessarily. Provide configurable retention policies, exportable audit reports per client or session, and time‑boxed access links for auditors. Surface a readable change history within the timeline and expose structured events via API/webhooks for compliance and external archiving.

Acceptance Criteria
Immutable Event Ledger for Thread Sync Actions
Given Thread Sync is enabled and a user performs a supported action (message ingested, message linked/unlinked/relinked, note revised, invoice updated, timeline message sent) When the action is committed Then an append-only audit event is persisted within 2 seconds containing event_id (UUIDv4), event_type, workspace_id, actor_id and actor_type, entity_type and entity_id, correlation_id, timestamp (UTC ISO-8601 ms), before, after, and relevant content hashes Given an audit event exists When any attempt is made to update or delete it via API, UI, or DB Then the operation is rejected with 405 and a new correction event is appended with reason and reference to the original event_id Given audit events exist for an entity When they are retrieved by API or timeline Then they are returned strictly ordered by a per-entity monotonic sequence and globally sortable by timestamp then event_id Given an event write fails transiently When retry logic runs Then the system retries up to 5 times with exponential backoff and marks the UI timeline item as Pending until success or PermanentFailure is raised
Message Integrity Hashing Without Content Exposure
Given a message is ingested from any channel When stored in the audit ledger Then SoloPilot stores message metadata (channel, sender, recipient, external_id, received_at) and a SHA-256 content_hash of the canonicalized body, but does not store the plaintext body in the ledger Given content canonicalization rules (normalize line endings to LF, trim trailing spaces, collapse consecutive whitespace) When hashing is computed Then the same input produces the same hash across channels and platforms Given a message hash exists When a verification request is made with the original content Then the verify endpoint returns Match when the SHA-256 of canonicalized content equals stored content_hash, else NoMatch, without persisting the submitted content Given message headers or bodies contain PII When audit events are created Then PII is not written to audit event fields; only hashes and non-sensitive metadata are stored; events that would leak PII are rejected and logged
Configurable Retention and Legal Hold
Given a workspace admin defines retention policies per category (messages, notes, invoices, audit_events) with durations between 30 and 3650 days When policies are saved Then changes are validated, versioned, recorded as audit events, and take effect at the next retention job run Given retention policies are active When the nightly purge job runs Then events older than their category retention are irreversibly purged within 24 hours, except items under legal hold Given a legal hold is placed on a client or session with a reason and scope When purge evaluation runs Then scoped items are excluded from purge and attempts to delete them via API return 423 Locked with a reference to the legal_hold_id Given an admin previews a policy change When requesting an impact report Then the system returns a count of items by category scheduled for purge and an estimated completion time
Exportable Audit Report by Client or Session
Given an admin selects a client or session and a date range When requesting an audit export Then the system generates a report in CSV or JSON containing all matching audit events ordered by timestamp then event_id, and produces a manifest with totals and a SHA-256 checksum for each file Given the matching set is ≤ 50,000 events When the export is requested Then the download link is available within 120 seconds; otherwise the request is queued and the admin is notified via in-app and email when ready Given an export is generated When it is downloaded Then the file is signed with a detached HMAC-SHA256 signature using the workspace export secret and the manifest includes the signature and key_id Given an export link is created When it expires after its TTL Then access returns 410 Gone and the file is removed from temporary storage
Time-Boxed Auditor Access Links
Given an admin creates an auditor access link scoped to a specific client, session, or date range When the link is issued Then it is read-only, uses a random 32-byte token, can be configured to require a passcode or OTP, and has a TTL between 1 and 30 days with a maximum download limit Given an auditor visits the link When authentication is satisfied Then the auditor can view and download only the scoped audit report and cannot see other clients, sessions, or PII; all access is logged with IP, user-agent, and timestamp Given a link is revoked or expires When access is attempted Then the system returns 401/410 and no data is leaked; revocation takes effect within 60 seconds globally
Readable Change History in the Thread Timeline
Given a user opens a client or session timeline When audit events exist Then each event is displayed with a human-readable summary (who, what, when, which entity), with expand/collapse to view before/after diffs and a link to raw JSON Given the timeline has more than 200 events When scrolled Then events are virtualized and load progressively, maintaining 95th percentile render times under 300 ms per batch Given a user has a preferred timezone When viewing timestamps Then the UI shows local time with a toggle to UTC; all timestamps include seconds and milliseconds Given the user searches or filters by event type, actor, or date range When applying filters Then the timeline updates within 500 ms and the current filter state is reflected in the URL for sharing
Audit Events API and Webhooks for External Archiving
Given an integrator calls GET /audit/events with filters (workspace_id required, optional entity_type, entity_id, event_type, actor_id, from, to) When the request is valid Then the API returns 200 with a paginated, stable schema (limit, next_cursor) ordered by timestamp then event_id; invalid filters return 400 with details Given webhooks are configured with a signing secret When new audit events are produced Then they are delivered to the webhook endpoint within 5 seconds (p95), signed with HMAC-SHA256 over the payload and timestamp header; receivers can verify the signature and reject replays older than 5 minutes Given webhook delivery fails with 5xx or timeout When retries occur Then the system retries up to 8 times with exponential backoff and jitter; idempotency is ensured via event_id; after max retries, the event is moved to a dead-letter queue and an alert is created Given the events schema evolves When version v2 is introduced Then both v1 and v2 are served for at least 180 days with versioning via Accept header or query param; deprecation notices are sent 90 days before removal
Permissions & Redaction Controls
"As a freelancer who collaborates with an assistant, I want to restrict what parts of a thread they can see so that client privacy is protected while work still gets done."
Description

Implement role‑based access controls and field‑level privacy settings that determine who can view message bodies, attachments, and sensitive note snippets in the timeline. Support roles such as Owner, Assistant, and Accountant, with customizable scopes (client‑level and session‑level). Provide redaction modes that show metadata only, blur sensitive sections, or require just‑in‑time reveal with reason logging. Enable shareable, expiring view‑only links that omit private notes while preserving the timeline structure. Log all sensitive views and redaction toggles to the audit trail, and honor workspace data‑minimization and retention settings.

Acceptance Criteria
Owner configures role-based access for Assistant (client-level)
Given an Owner assigns an Assistant a client-level scope for Client A with message bodies and attachments disabled When the Assistant opens Client A’s Thread Sync timeline Then the Assistant can view message metadata (sender, timestamp, subject, delivery status) for all items under Client A And the Assistant cannot open message bodies or attachments unless explicitly granted field access And redaction indicators are shown in place of hidden fields And the Assistant cannot access timelines for clients other than Client A And all access checks and denials are recorded in the audit trail with user, role, client, and item identifiers
Accountant restricted to a single session within a client
Given an Accountant has a session-level scope for Session S belonging to Client A When the Accountant opens Client A’s Thread Sync timeline Then only Session S appears fully visible with preserved chronological placement And items from other sessions for Client A render as “Restricted” placeholders that maintain timeline order and timestamps And invoice and payment metadata for Session S are visible, while message bodies, attachments, and private notes remain redacted by default And exports initiated by the Accountant include only Session S data And all visibility decisions are captured in the audit trail
Redaction modes: metadata-only, blur, and just-in-time reveal
Given a timeline item contains sensitive note snippets and is redaction-enabled When a permitted user switches to Metadata-only mode Then the item shows headers and non-sensitive fields only, with bodies and attachments replaced by placeholders When the user switches to Blur mode Then sensitive sections are obscured while preserving layout and are non-selectable/non-copyable When the user invokes Just-in-time Reveal Then a reason text (minimum 10 characters) is required and the reveal duration is time-boxed (e.g., 15 minutes) before auto-reversion to the prior redaction mode And all redaction toggles and reveals are logged with timestamp, user, role, item ID, and reason And users without reveal permission see an access denied message and no content change
Shareable expiring view-only link preserves timeline structure
Given an Owner generates a view-only link for Client A’s timeline with a 48-hour expiration When a recipient opens the link before expiration Then the timeline structure and chronology are preserved with private notes omitted and sensitive fields redacted And message bodies and attachments are hidden by default, showing metadata-only placeholders And the interface disables editing, downloading, and copy of redacted content And each view event is logged with link ID, viewer fingerprint, timestamp, and items accessed When the link is opened after expiration or revoked Then access is blocked and the attempt is logged
Audit trail logs sensitive views and redaction toggles
Given any sensitive view, permission denial, or redaction-mode change occurs Then an immutable audit entry is created with timestamp, acting user, role, client, session, item ID, action, reason (if applicable), IP/device, and outcome And audit entries are queryable by date range, user, role, client, session, and action type And audit exports are available in CSV and JSON and reflect applied filters And tampering with or deleting audit entries is not permitted by any role And audit retention follows workspace policy and indicates scheduled purge dates
Retention and data minimization enforced on timeline items
Given a workspace retention policy of 180 days for message bodies and 365 days for attachments is active When items exceed their retention thresholds Then message bodies and/or attachments are purged or anonymized while preserving timeline order and non-sensitive metadata And view-only links referencing purged content automatically exclude it and remain structurally intact And search indices and exports omit purged fields And a visible placeholder notes that content was removed per policy And purge events are logged in the audit trail with policy references
Send updates/reschedule links without exposing private notes
Given an Assistant with client-level scope composes a reschedule/update from the Thread Sync timeline When inserting timeline context into the outbound message Then only metadata (subject, date/time, session ID) is inserted; private notes and sensitive snippets are never included And the preview matches the final sent content with all redactions honored And sending is restricted to clients/sessions within the Assistant’s scope And the outbound message is recorded in the timeline without exposing redacted content to unauthorized roles

Burndown Bar

A live, color‑coded bar on every client and package that shows remaining credits or minutes at a glance. It updates the moment a session is logged or edited and highlights projected run‑out dates based on upcoming bookings. Eliminates spreadsheet checks, makes status instantly visible during scheduling, and prevents surprise overages.

Requirements

Real-time Balance Computation
"As a solo practitioner, I want remaining package minutes to update the moment I log or edit a session so that I always see an accurate balance without refreshing or checking spreadsheets."
Description

Implement a server-side balance engine that calculates remaining credits/minutes per client and per package in real time. The engine listens to lifecycle events (session created/edited/canceled, manual adjustment, refund, invoice posted) and recalculates balances immediately with idempotent, transactional updates. Support both minutes and credit-based packages, variable session lengths, partial sessions, and rounding rules aligned with invoicing. Maintain a single source of truth via a balance ledger that records all debits/credits, enabling consistent recomputation and reconciliation. Emit change notifications to the UI (e.g., WebSocket/SSE/pub-sub) so the burndown bar updates without page refresh. Handle retroactive edits and soft/hard deletes by replaying ledger entries to guarantee accuracy. Ensure timezone-safe computations and guard against race conditions during concurrent edits.

Acceptance Criteria
Real-Time Update on New Session (Minute Package)
Given a client has an active minute-based package with 300 remaining minutes and a configured rounding rule When a 60-minute session is created and saved with eventId S123 Then the balance ledger records a debit of 60 minutes with source=session.create and eventId S123 And the remaining balance updates to 240 minutes in the same transaction And a balance-change notification is emitted to the client's UI channel within 2 seconds And repeating the same session.create with eventId S123 does not create an additional ledger entry or change the balance And the burndown bar displays 240 minutes without page refresh
Session Edit Recomputes Balance with Rounding Alignment
Given an existing session with recorded rounded debit of 45 minutes and an account rounding rule "round up to 15-minute increments" When the session duration is edited from 45 to 50 minutes and saved with eventId S124 Then the engine computes the new rounded debit as 60 minutes and appends a compensating adjustment of +15 minutes (source=session.edit, refSessionId, eventId S124) And the prior ledger entry remains immutable And the remaining balance decreases by exactly 15 minutes in the same transaction And repeating the same session.edit with eventId S124 is idempotent (no duplicate adjustments) And the burndown bar updates within 2 seconds without page refresh
Cancellation/Refund Adjusts Balance Idempotently
Given a session previously debited 60 minutes and invoiced, and a cancellation with refund is issued with eventId S125 When the cancellation is processed Then the ledger appends a credit of 60 minutes (source=session.cancel, eventId S125) linked to the invoice And the remaining balance increases by 60 minutes in the same transaction And if the session is hard-deleted, a reversing ledger entry is appended; if soft-deleted, the original debit is retained and annotated, with a separate credit entry And retrying the cancellation with the same eventId S125 does not double-credit And the burndown bar reflects the increased balance within 2 seconds without page refresh
Concurrent Edits Race-Safe Transactional Update
Given two users concurrently submit edits to the same session (Edit A to 30 minutes, Edit B to 45 minutes) with distinct eventIds A and B When both edits reach the server within 100 ms Then only the edit that corresponds to the final persisted session version mutates the ledger; the other is rejected or retried based on version conflict policy And the ledger contains at most one adjustment for that session version, with no duplicate or conflicting debits/credits And the resulting remaining balance equals the baseline minus the final rounded debit for the session And no transient negative balances occur And the burndown bar shows only the final balance value after processing completes
Retroactive Edit Ledger Replay and Reconciliation
Given a session from 30 days ago is edited today with eventId S126 When the engine processes the edit Then it replays and recomputes the affected package's ledger from the edit point forward deterministically And the end balance equals starting balance + sum(credits) − sum(debits) across all entries, within 0 difference And the burndown bar and projected run-out date recalculate accordingly and update within 2 seconds And unrelated clients and packages remain unaffected And the operation is idempotent for repeated eventId S126
Credit-Based Package with Partial Sessions
Given a client has a credit-based package with 10 credits remaining and a rule allowing 0.5-credit partial sessions When a 30-minute session consuming 0.5 credit is created and saved with eventId S127 Then the ledger records a debit of 0.5 credit with source=session.create and eventId S127 And the remaining balance updates to 9.5 credits in the same transaction And rounding is applied per configured credit rounding rule And the burndown bar displays 9.5 credits within 2 seconds And repeating the same eventId S127 is idempotent
Timezone-Safe Computations and DST Handling
Given the account timezone is America/New_York and a session runs from 1:30 AM to 2:30 AM on a DST start date When the engine computes duration and applies rounding Then the billed duration equals 60 minutes based on actual elapsed time, independent of the clock change And all ledger timestamps are stored in UTC with the account timezone captured for computation And balances and projected run-out are identical for viewers in different timezones And sessions crossing midnight are attributed to the correct package/day per account timezone rules And replaying the ledger yields the same balance before and after a DST transition
Color-coded Burndown UI Bar
"As a scheduler, I want a clear, color-coded bar that shows remaining credits at a glance so that I can schedule confidently and avoid overbooking."
Description

Create a reusable, responsive UI component that visually represents remaining package balance as a horizontal bar with threshold-based colors. Default thresholds: green >50%, amber 10–50%, red ≤10% and striped red when overdrawn. Display precise remaining units (e.g., "3 sessions" or "45 min") and total purchased, with a tooltip showing last activity and next booked session. Ensure WCAG 2.1 AA contrast, provide text labels and ARIA roles for screen readers, and support theme/dark mode. Make the bar clickable to open a detailed balance drawer with the ledger and upcoming bookings. Include empty, loading, and error states, and support localization of units and formatting.

Acceptance Criteria
Threshold Colors and Overdraw Pattern
Given a package with remaining percentage > 50 When the burndown bar renders Then the bar fill color is green Given a package with remaining percentage > 10 and ≤ 50 When the burndown bar renders Then the bar fill color is amber Given a package with remaining percentage > 0 and ≤ 10 When the burndown bar renders Then the bar fill color is solid red Given a package with remaining percentage < 0 When the burndown bar renders Then the bar fill is red with a diagonal stripe overdraw pattern Given remaining percentage equals 50 When the burndown bar renders Then the color is amber Given remaining percentage equals 10 When the burndown bar renders Then the color is red Given the remaining percentage crosses a threshold due to a balance change When the component updates Then the color updates to the correct threshold color within 1 second without page refresh
Precise Units and Totals Display
Given a sessions-based package (e.g., 10 total, 3 remaining) When the burndown bar renders Then a text label displays "3 of 10 sessions remaining" with correct singular/plural grammar Given a time-based package in minutes (e.g., 300 total, 45 remaining) When the burndown bar renders Then a text label displays "45 of 300 minutes remaining" Given any package When the burndown bar renders Then the percentage remaining is also displayed as text (e.g., "15% remaining") adjacent to or within the bar Given very narrow containers When the label would truncate Then the label truncates gracefully with ellipsis and the full value is available via tooltip or accessible name
Live Update on Session Log or Edit
Given a user saves a new session that deducts units from the package When the save succeeds Then the remaining units and percentage recalculate and the bar updates within 1 second without page refresh Given a user edits an existing session to change its duration/units When the save succeeds Then the bar updates to reflect the new remaining balance within 1 second Given a user deletes a session that had deducted units When the delete succeeds Then the bar updates to restore units within 1 second Given multiple rapid consecutive changes by the same user When the final save succeeds Then the bar reflects the latest persisted balance (no intermediate stale state persists)
Tooltip Shows Last Activity and Next Booking
Given the burndown bar is focused or hovered When the user pauses for up to 300 ms Then a tooltip appears showing: Last activity timestamp and Next booked session datetime (if any) Given there is no next booked session When the tooltip appears Then it displays "No upcoming sessions" in the next session field Given the user’s locale and timezone settings When the tooltip renders timestamps Then dates and times are formatted in the user’s locale and timezone Given touch devices When the user taps the info affordance or long-presses the bar Then the same tooltip content is displayed and dismissible with tap outside or close control
Accessibility, Theming, and Responsiveness
Given the burndown bar in light and dark themes When rendered Then all text on or adjacent to the bar has ≥ 4.5:1 contrast and essential non-text UI parts (bar fill/stripes) have ≥ 3:1 contrast against adjacent colors (WCAG 2.1 AA) Given assistive technology users When the bar receives focus Then it exposes role="progressbar" with aria-valuemin=0, aria-valuemax=100, aria-valuenow set to the integer percent, and aria-valuetext describing remaining and total units and overdrawn state when applicable Given keyboard navigation When tabbing to the bar Then a visible focus indicator is shown and pressing Enter or Space activates the bar click action Given color-blind users When the bar is rendered Then remaining status is conveyed by text/aria in addition to color (color is not the sole indicator) Given small containers (≤ 320px width) When the bar renders Then content remains readable without overlap, and tap/click target size is ≥ 44×44 CSS px
Clickable Bar Opens Balance Drawer
Given the burndown bar is visible When the user clicks it or activates it via Enter/Space Then a balance drawer opens from the edge of the screen Given the balance drawer opens When content loads Then it displays: current remaining and total, a chronological ledger of debits/credits (most recent first), and a list of upcoming bookings (soonest first) Given slow network conditions When the drawer is opened Then a loading skeleton is shown until data arrives and initial content renders within 2 seconds at p95 Given the drawer is open When the user presses Esc, clicks the close control, or taps the scrim Then the drawer closes and focus returns to the bar Given a data fetch error for the drawer When the drawer is opened Then an inline error message with a Retry action is shown
Empty, Loading, and Error States
Given package data is still fetching When the bar’s container renders Then a loading state is shown (skeleton bar with shimmer) and no stale values are displayed Given no active package exists for the client/context When the component renders Then an empty state is shown with text "No active package" and the bar is not interactive Given a fetch or compute error occurs When the component renders Then an error state is shown with a descriptive message and a Retry control; the bar is not interactive Given recovery from error or empty states When valid data becomes available Then the standard bar replaces the state within 1 second without page reload
Localization of Units and Formatting
Given the app locale is set (e.g., en-US, en-GB, fr-FR) When the bar renders text Then numbers are formatted per locale (thousands separators, decimal marks) and unit labels (session/sessions, minute/minutes) are correctly pluralized in that locale Given RTL locales (e.g., ar, he) When the bar and tooltip render Then layout and text direction are RTL compliant and remain legible Given timestamps in the tooltip When rendered Then they use the user’s timezone and locale-specific date/time formats Given localization updates at runtime When the user changes locale Then the bar, tooltip, and drawer texts re-render in the new locale without page refresh
Run-out Date Projection
"As a coach, I want to see when a client’s package will run out based on upcoming sessions so that I can propose renewals or adjust scheduling before we hit zero."
Description

Build a projection service that estimates the date a client/package will reach zero based on upcoming bookings and historical session durations. Use scheduled events’ durations and package rules to forecast depletion, accounting for tentative holds, recurring series, timezones, and buffer rules. Surface a concise indicator (e.g., "Projected to run out by Oct 28; 2 sessions remaining after next week’s bookings") adjacent to the burndown bar. Recalculate instantly on scheduling changes and edits, and display an uncertainty hint when assumptions are applied (e.g., variable session lengths). Provide APIs to fetch projections for list views to avoid N+1 calls, with caching and invalidation tied to calendar and session updates.

Acceptance Criteria
Instant Projection Update on Schedule Change
Given a client/package is visible with the burndown bar and projection indicator When a session is created, edited, deleted, or its status changes (tentative ↔ confirmed) and the change is persisted Then the projection service recalculates within 1 second of persistence And the UI indicator adjacent to the burndown bar updates within 2 seconds to reflect the new projected run-out date and remaining count And a hard refresh shows the same updated value (no stale cache) And p95 recalculation latency across 100 concurrent updates is ≤ 1.5 seconds and p95 UI update time is ≤ 2.5 seconds
Accurate Depletion Forecast Using Package Rules
Given packages can be credits-based or minutes-based with configured rounding increment, minimum billable, and buffer rules When forecasting depletion from upcoming bookings Then future sessions consume scheduled duration/credits adjusted by package rules (buffer subtracted first, remaining rounded up to increment, minimum applied) And if remaining balance is insufficient for a future session, the run-out date equals the date of the session that causes exhaustion and the indicator reflects mid-session stop/overage And the indicator text format is: "Projected to run out by {Mon DD}; {X} {sessions|minutes} remaining after next week’s bookings" with correct pluralization And for minutes packages, remaining shows integer minutes; for credits packages, remaining shows integer sessions/credits
Handling Tentative Holds and Recurring Series
Given future sessions may be marked tentative and series may recur When computing the projection Then tentative sessions are included in consumption and the projection displays an uncertainty hint And converting or canceling a tentative session triggers immediate recalculation with updated projection And all generated occurrences of a recurring series within the projection window are included And edits to the series (time, count, exceptions) update the projection within 2 seconds
Timezone and DST-Safe Run-out Date
Given the package has a configured timezone and viewers may have different locales/timezones When computing consumption and displaying the run-out date across DST transitions Then consumption uses the package timezone rules for all sessions And the displayed date/time uses the viewer’s locale/timezone formatting while preserving the calculation based on the package timezone And sessions crossing DST changes consume their scheduled durations (no DST-induced over/under-count) And automated tests cover spring-forward and fall-back cases in at least three distinct timezones
Uncertainty Hint Display and Content
Given variable session lengths, presence of tentative holds, or limited history can affect confidence When any of the following is true: historical actuals vary > 20% from scheduled, ≥1 tentative hold is included, or < 3 completed sessions exist in the past 90 days Then an "Uncertain" hint is shown next to the projection And the hint/tooltip lists concrete reasons, e.g., "Tentative holds included (2); Variable durations; Limited history" And when none of these conditions are true, the hint is not shown And the hint opens within 150 ms on hover/focus, is keyboard navigable, and includes an ARIA label describing the reasons
Batch Projections API with Caching and Invalidation
Given list views need projections for multiple clients/packages When requesting projections for up to 200 IDs via a single bulk endpoint Then the response returns all requested projections in one call (no N+1) And p95 latency is ≤ 300 ms for up to 50 IDs and ≤ 600 ms for up to 200 IDs And responses include a version and cache key And server-side cache TTL is 60 seconds and is invalidated within 2 seconds of calendar/session updates for affected IDs only
Consistency Under Concurrent Updates
Given two or more users make overlapping scheduling changes for the same client/package When changes are saved in quick succession Then the final projection reflects the last persisted state (last-write-wins) And no duplicate consumption is counted And the burndown bar totals reconcile with the projection after a refresh And event-driven recalculations are idempotent and retried up to 3 times with exponential backoff on transient failure, with errors logged and monitored
Multi-surface Placement & Sync
"As a user, I want the burndown status visible wherever I schedule or review a client so that I never miss low-balance signals during workflows."
Description

Embed the burndown bar consistently on Client Profile, Package Details, and the Scheduling/Booking modal so balance is visible wherever work happens. Ensure instant visual updates across all surfaces when sessions are logged or edited via real-time subscriptions. Provide deep links from the bar to package detail and invoice pages. Include skeleton/loading states to avoid layout shift, and gracefully handle clients without an active package (e.g., show "No active package" with CTA to sell/assign). Support responsive layouts for desktop and mobile web, and implement feature flags to progressively roll out placement by surface.

Acceptance Criteria
Consistent Burndown Bar Placement Across Key Surfaces
- Given a user with permission to view client billing, When they open a Client Profile with an active package, Then the burndown bar renders in the header area above session history and displays the correct remaining credits/minutes. - Given a Package Details page for the same package, When the page loads, Then the burndown bar renders at the top of the details panel with identical values, labels, and color state as on the Client Profile. - Given the Scheduling/Booking modal opened for the same client, When the modal opens, Then the burndown bar appears in the modal summary/header and shows identical values to other surfaces. - Given design consistency, When the bar is compared across surfaces, Then label text, tooltip copy, color mapping, and value formats are identical.
Real-Time Cross-Surface Sync on Session Changes
- Given an active package shown on multiple open surfaces or tabs, When a session that consumes credits/minutes is logged, Then each visible burndown bar updates within 1 second of backend confirmation via real-time subscription without manual refresh. - Given a session’s duration or applied credits is edited, When the change is saved, Then the remaining balance and color state recalculate and update across all open surfaces within 1 second. - Given a session is canceled or deleted, When the change is saved, Then consumed credits/minutes are returned and the bar updates accordingly across all open surfaces within 1 second. - Given the subscription connection drops, When a change occurs, Then the bar updates via fallback (poll or refresh) within 15 seconds and shows a non-blocking reconnecting indicator until the connection is restored.
Deep Links from Bar to Package and Invoice Pages
- Given the bar is visible, When the user clicks the “View package” link, Then they navigate to the correct Package Details page for the displayed package within the same workspace/tenant. - Given the bar is visible, When the user clicks the “Invoices” link, Then they navigate to the invoices view pre-filtered to the client and package (if applicable). - Given role-based permissions, When a user without invoice access views the bar, Then the invoice link is hidden; and if accessed via direct URL, Then a friendly access-denied state is shown. - Given standard browser behavior, When the user Ctrl/Cmd-clicks a deep link, Then it opens in a new tab and preserves context via URL parameters.
Skeleton/Loading States and Layout Stability
- Given any supported surface, When bar data is loading, Then a skeleton placeholder with reserved height renders within 100 ms to prevent content jump. - Given the skeleton is shown, When data arrives, Then the skeleton is replaced by the bar without shifting surrounding elements (cumulative layout shift delta ≤ 0.1 in the bar region). - Given a fetch or subscription error, When loading fails, Then a non-blocking inline error state with a Retry control is shown in the reserved space without collapsing layout. - Given rapid navigation between clients/packages, When switching context, Then the previous bar is cleared and the skeleton is shown until fresh data is rendered (no stale values).
Graceful Handling of Clients Without an Active Package
- Given a client without an active package, When a supported surface loads, Then the bar area displays “No active package” and a primary CTA labeled “Sell/Assign package”. - Given role-based permissions, When the viewer lacks rights to create/assign packages, Then the CTA is disabled or hidden and a tooltip or helper text explains required permissions. - Given the Scheduling/Booking modal for such a client, When booking proceeds, Then scheduling is allowed and a non-blocking notice suggests assigning a package first. - Given an active package is assigned while the surface is open, When the assignment completes, Then the bar auto-switches from “No active package” to the active bar state in real time.
Responsive Layouts for Desktop and Mobile Web
- Given viewport ≥ 1024 px width, When the bar renders, Then full label, numeric balance, color bar, and deep links display inline without truncation or overlap. - Given viewport ≤ 375 px width, When the bar renders, Then text truncates with ellipsis, deep links collapse into an overflow control, and all tap targets are ≥ 44×44 px. - Given device rotation or window resize, When the viewport changes, Then the bar reflows without overflow/overlap and maintains legibility and correct values. - Given high-DPI displays, When the bar renders, Then icons and the color bar appear crisp with no pixelation or blurry scaling.
Feature Flags for Progressive Rollout by Surface
- Given feature flags burndownBar.clientProfile, burndownBar.packageDetails, and burndownBar.schedulingModal, When a flag is enabled for a tenant, Then the bar renders only on the corresponding surface(s). - Given a surface flag is disabled, When the surface loads, Then the bar does not render and no network calls for bar data are made for that surface. - Given a flag’s state changes at runtime, When the page is reloaded or the modal is reopened, Then the new flag state takes effect without redeploy. - Given percentage-based rollout, When the flag is set to a cohort < 100%, Then only tenants/users in the cohort see the bar per flag rules.
Balance Ledger & Backfill Migration
"As an admin, I want an auditable history of changes to a client’s balance so that I can reconcile invoices and correct errors without data drift."
Description

Introduce a normalized balance ledger that records atomic debit/credit events with metadata (source, session ID, invoice ID, actor, timestamp, notes). Provide a one-time migration to backfill ledger entries from historical sessions, invoices, and adjustments so existing clients/packages show correct balances on day one. Add audit trails and an admin-only correction flow for manual adjustments with required reasons to prevent data drift. Implement reconciliation checks to ensure ledger totals align with invoiced amounts and package definitions, with alerts for discrepancies. Preserve referential integrity and version packages to handle rule changes over time.

Acceptance Criteria
Atomic Ledger Event Recording
Given a client package P123 with 300 minutes and a session S456 logged for 60 minutes by user U1 When the session is saved Then exactly one debit ledger entry exists for P123 linked to S456 with amount 60, source "session", actor U1, and a system timestamp within 1 second of save And the ledger entry stores immutable metadata (source, session_id, invoice_id nullable, actor_id, timestamp, notes nullable) And subsequent edits to S456 create compensating ledger entries (crediting the prior amount and debiting the new amount) rather than mutating the original row And deleting S456 creates a compensating credit equal to the original debit, preserving a complete audit trail
Historical Backfill Migration Produces Correct Balances
Given historical sessions, invoices, and adjustments exist prior to the ledger go‑live When the one‑time backfill migration is executed in production mode Then ledger entries are created for all eligible historical records with correct source links and timestamps based on original event times And for every client/package, computed remaining balance (sum of credits + adjustments − debits) exactly matches the expected balance from historical data with zero discrepancy And a migration report is generated listing counts by source type, created rows, skipped rows with reasons, and per‑entity balance checks And re‑running the migration in idempotent mode produces no duplicate ledger rows and no net balance change And running in dry‑run mode performs all validations and outputs the report without writing any rows
Admin-Only Manual Adjustment With Required Reason and Audit Trail
Given a non‑admin attempts to create a manual adjustment on package P123 Then the request is rejected with 403 Forbidden and no ledger entry is written Given an admin initiates a +30 minute adjustment on P123 with a required reason and a note of at least 10 characters When the admin submits the adjustment Then a single ledger entry is written with source "manual_adjustment", amount +30, actor set to the admin, required reason captured, and a system timestamp And the adjustment is visible in the audit log with actor, timestamp, reason, and note And manual adjustments are immutable and can only be reversed by a new manual adjustment entry with its own required reason
Automated Reconciliation and Discrepancy Alerts
Given the nightly reconciliation job runs When it compares per‑package ledger totals to invoiced entitlements and package definitions Then any non‑zero discrepancy in units or currency creates a discrepancy alert with client_id, package_id, discrepancy amount, first_detected_at, and links to the offending entries And packages with no discrepancy remain unalerted and are logged as Passed And resolving an alert requires a note and is automatically marked Resolved only after the next reconciliation run confirms a zero discrepancy
Referential Integrity and Constraint Enforcement
Given a ledger insert references session_id S999 that does not exist When the insert is attempted Then it is rejected and no row is written And ledger rows that include session_id, invoice_id, package_id, and actor_id must reference existing rows or be null only when not applicable, enforced by foreign keys And attempts to delete a session, invoice, package, or user referenced by any ledger row are blocked with a 409 Conflict (or soft‑delete preserves referential integrity), and the ledger remains intact
Package Versioning Applied Over Time
Given package P123 has version V1 (effective T1) and later changes to version V2 (effective T2 > T1) When a session is logged at time T < T2 Then the resulting debit ledger entry references P123@V1 and debits according to V1 rules When a session is logged at time T >= T2 Then the resulting debit ledger entry references P123@V2 and debits according to V2 rules And historical ledger entries continue to reference their original package version and are not retroactively recalculated And balance calculations "as of" a date use only entries and package rules effective on or before that date
Real-Time Burndown Bar Reflects Ledger Changes
Given a user logs, edits, or deletes a session or applies an invoice that changes entitlements for package P123 When the operation completes Then the Burndown Bar for P123 updates within 2 seconds to reflect the new remaining minutes/credits based on the ledger sum And the projected run‑out date is recalculated using upcoming bookings and the updated remaining balance And concurrent edits from multiple users produce a single consistent Burndown state with no stale values on refresh
Performance, Concurrency & Caching
"As a busy practitioner, I want the burndown bar to update instantly and reliably even during peak hours so that I can trust what I see while multitasking."
Description

Meet a sub-200ms p95 target for balance recompute and UI update after session events, scaling to peak scheduling hours without regressions. Use optimistic UI where safe, with eventual consistency guards and automatic refresh on server confirmation. Implement concurrency controls (e.g., per-client/package locks) to prevent double-counting under simultaneous edits. Provide read-through caching for computed balances and projections with precise invalidation on session/calendar changes. Add observability (metrics, logs, traces) and synthetic tests to detect staleness or slowdowns, plus rate limiting and circuit breakers to protect core scheduling flows.

Acceptance Criteria
p95 Sub-200ms Burndown Update After Session Events
Given a client or package view with the Burndown Bar visible And the user creates, edits, or cancels a session for that client/package When the user presses Save Then the Burndown Bar visually reflects the new balance within 200ms at p95 across ≥1,000 sampled events under peak-hour load And balance math equals the server-confirmed value with no discrepancy > 0 credits or > 0 minutes And no frame jank exceeds 50ms at p95 during the update
Optimistic UI With Server Confirmation Auto-Refresh
Given optimistic UI is enabled for session mutations When a session create/edit/cancel is initiated Then the Burndown Bar applies an optimistic delta within 50ms and displays a syncing state And upon server confirmation the UI reconciles to the confirmed balance within 50ms and removes the syncing state And upon server rejection the optimistic delta is reverted within 100ms and an inline error is shown And if client and server balances differ for > 2s, the UI auto-refreshes the balance and projection without user action
Concurrency Control Prevents Double-Counting Under Simultaneous Edits
Given two or more concurrent mutations target the same client/package When both attempt to create or modify sessions that change the same balance window Then per-client/package locking serializes the commits so only one mutation applies at a time And the losing committer receives a 409 Conflict with retry guidance and no partial application And the net balance delta equals the sum of committed mutations exactly once (no double-counting) And idempotency keys prevent duplicate application on retries And lock wait time p95 is ≤ 150ms under peak load
Accurate Read-Through Cache With Precise Invalidation
Given read-through caching is enabled for balances and projections When a session or calendar change affecting a client/package is committed Then the cache entry for that client/package is invalidated within 50ms and recomputed on next read And cross-node readers observe the updated value within 100ms at p95 And no stale read persists more than 100ms after the commit And cache hit ratio is ≥ 90% during normal operations And cache stampede protection ensures only 1 recompute runs per key concurrently
Projection Refresh and Run-Out Date Accuracy
Given the client/package has future bookings When a booking is created, moved, extended, shortened, or canceled Then the projected run-out date/time and color state update within 200ms at p95 And the projection uses the same timezone and rounding rules as the scheduler And the run-out date/time matches a full recomputation within ±1 minute And nearing/at/overrun color thresholds match specification for remaining credits/minutes
Observability and Synthetic Staleness Detection
Given production telemetry is enabled When Burndown updates are processed Then metrics are emitted for update latency (p50/p95/p99), cache hit rate, staleness age, lock wait time, conflict rate, and errors And distributed traces include spans for compute, cache read/write, lock acquire, and UI render correlated by client/package ID And logs contain request IDs and idempotency keys for all mutations And synthetic canaries run every 1 minute to create/edit/cancel sessions and assert p95 ≤ 200ms and max staleness age ≤ 2s, with alerts within 5 minutes on breach
Rate Limiting and Circuit Breaking Protect Scheduling Flows
Given elevated traffic or downstream degradation occurs When mutation rates exceed quotas or dependency latency exceeds 500ms p95 for 2 minutes Then per-tenant and per-IP rate limits return 429 with Retry-After while preserving UI responsiveness And circuit breakers open for degraded dependencies, serving cached balances with a visible degraded state and queuing mutations safely And core scheduling error rate remains ≤ 1% and median Burndown update latency ≤ 250ms during the incident And upon recovery, queued mutations are applied exactly once and the UI reconciles within 5 minutes

Depletion Forecast

Predicts when a client will hit zero based on cadence, booked sessions, and average session length. Triggers proactive low‑credit alerts to you and the client with suggested top‑up amounts and one‑tap renewal options. Keeps engagements smooth, avoids mid-month pauses, and helps you plan capacity with confidence.

Requirements

Unified Credit Ledger & Consumption Rules
"As a solo practitioner, I want all client credits tracked in one accurate ledger so that forecasts and invoices reflect the true remaining balance without manual reconciliation."
Description

Implement a single source of truth for client balances that supports hours, sessions, and monetary credits with configurable conversion rules. The ledger must ingest and reconcile usage from booked sessions, actual session durations, timesheet entries, and invoice line items, applying rounding rules and proration based on average session length. It must support manual adjustments, backdating, refunds/chargebacks, and currency handling where applicable. Real-time balance must be queryable by other SoloPilot services, and balance updates should trigger events for forecasts and automations. This foundation ensures accurate, up-to-date balances that the Depletion Forecast can reliably use to predict depletion dates and remaining coverage.

Acceptance Criteria
Multi-Unit Credits with Conversion Rules
Given a client has balances: 4 sessions, 8.0 hours, and $600 USD, with conversion rules (sessions debit first; if insufficient, spill to hours; if insufficient, convert $ to hours at $150/hour; overdraft disabled; average session length 60 minutes) When a 90-minute session is posted for consumption Then the ledger debits 1 session and 0.5 hours, and $ remains unchanged And post-transaction balances are: 3 sessions, 7.5 hours, $600 USD And the consumption record stores the conversion rate ($150/hour) and rule_version used And no balance becomes negative
Session Consumption with Rounding and Proration
Given rounding increment is 0.25 hours and consumption is based on actual duration When a session scheduled for 60 minutes completes at 50 minutes Then the ledger debits 0.75 hours (rounded up from 50 minutes) from the applicable pool(s) And usage metadata includes actual_minutes=50, rounded_to_hours=0.75, rounding_rule_id When a session scheduled for 60 minutes completes at 73 minutes Then the ledger debits 1.25 hours from the applicable pool(s) And usage metadata includes actual_minutes=73, rounded_to_hours=1.25
Source Reconciliation: Bookings, Durations, Timesheets, Invoices
Given a booked session (id S1) has an actual duration of 55 minutes, a timesheet entry referencing S1 for 1.0 hour, and an invoice line item referencing S1 for 1.0 hour When the ledger ingests records for S1 from all sources Then only 1.0 hour of usage is applied once And the precedence applied is invoice > actual_duration > scheduled > timesheet And the consumption record links source_ids [invoice_line_id, session_id, timesheet_id] And ingestion uses idempotency keys per source to avoid double-application on retries Given a timesheet entry (no booking, no invoice) for 2.0 hours When ingested Then the ledger debits 2.0 hours subject to rounding rules
Manual Adjustments and Backdating with Audit Trail
Given an admin with permission Credit:Adjust posts a +2.0 hours manual adjustment effective 2025-08-15 with reason "Goodwill" When saved Then the hours balance increases by 2.0 as of 2025-08-15 and is reflected in current balance And an immutable audit record is created with actor_id, reason, note, created_at, effective_at, unit, delta, before_balance, after_balance, adjustment_id And the adjustment cannot be edited in place; reversal requires a compensating -2.0 hours entry linked to adjustment_id And backdated adjustments trigger re-computation of depletion forecasts Given overdraft is disabled When a manual debit would push any unit below zero Then the operation is rejected with HTTP 409 and no changes are applied
Refunds and Chargebacks with Currency Handling
Given a purchase created 10.0 hours at $150/hour for $1500 USD on 2025-09-01 with stored fx_rate_EURUSD=0.90 When a full refund is processed Then 10.0 hours are removed and $1500 USD is reversed using the original fx_rate, and balances do not go below zero And the refund record links to the original purchase and captures amounts in both USD and EUR Given a $300 USD chargeback is processed against the same purchase When applied Then the ledger reduces 2.0 hours (based on $150/hour) if available; if fewer than 2.0 hours remain, the remaining hours are removed and the shortfall is recorded as negative money balance only if overdraft is enabled; otherwise the chargeback is rejected
Real-Time Balance API: Accuracy and Performance
Given no pending transaction for client C1 When GET /v1/clients/C1/balance is called Then the response includes totals and breakdown by unit (sessions, hours, money), rule_version, and as_of timestamp with millisecond precision And the read is strongly consistent with the last committed write And the p95 latency is <= 250 ms under nominal load And the API supports GET /v1/clients/C1/balance?as_of=2025-08-31T23:59:59Z returning a historical balance
Balance Update Events: Schema, Idempotency, Delivery
Given a balance-changing transaction is committed for client C1 When the transaction commits Then an event is published within 2 seconds to the event bus with payload fields: event_id (UUID), occurred_at, client_id, deltas [{unit, amount, currency}], balances_after [{unit, amount, currency?}], source_type, source_id, correlation_id, rule_version And events are at-least-once and ordered per client_id And duplicate deliveries carry the same event_id to enable consumer deduplication And no event is published for idempotent replays that do not change balances And the Depletion Forecast service updates the client's projection within 5 seconds of event receipt
Forecast Engine with Assumptions & Buffers
"As a practitioner, I want a clear forecast of when a client will run out of credits so that I can proactively secure renewals and avoid mid‑month service pauses."
Description

Build a computation engine that predicts each client’s depletion date using scheduled sessions, historical cadence, and average session length. The engine must handle multiple units (hours, sessions, currency), support configurable assumptions (e.g., target cadence, session length variance, holidays), and apply safety buffers (e.g., X% variance or N extra minutes per session). Forecasts should update in real time on booking changes and nightly for cadence recalibration, outputting key metrics: depletion date, days remaining, projected usage by week, and risk tier (low/medium/high). Provide explainability data (drivers and assumptions) for UI display and audit.

Acceptance Criteria
Real-Time Update on Booking Changes
Given a client with an existing forecast, when a new session is booked with duration D in unit U, then the engine recalculates within 5 seconds and updates depletion_date, days_remaining, projected_usage_by_week, and risk_tier. Given a client with an existing forecast, when a scheduled session is edited (date, time, duration) or canceled, then the engine recalculates within 5 seconds and the updated outputs reflect the change. Given multiple booking changes arrive within 10 seconds, when processing completes, then the forecast reflects all changes and shows the latest state (eventual consistency within 10 seconds). Given a booking change that does not alter total consumption (e.g., reschedule within same week and same duration), then projected_usage_by_week totals remain unchanged for that week. Given any recalculation due to booking changes, then the forecast payload includes change_reason = "booking_change" and increments forecast_version by 1.
Nightly Cadence Recalibration
Given completed sessions exist in the historical record, when the nightly recalibration runs, then the engine recomputes average session length and cadence using the last L completed weeks (default L=8) and updates the forecast. Given fewer than 3 completed weeks are available, when recalibration runs, then the engine falls back to configured target_cadence and average_session_length assumptions. Given the nightly job, when it executes, then it runs once per 24 hours and completes by 04:00 in the workspace timezone and records last_run_at. Given a recalibration update, then forecasts are updated and change_reason = "nightly_recalibration" is recorded. Given the nightly job runs, then no duplicate recalculation occurs for the same date window (idempotent by last_run_at).
Multi-Unit Handling (Hours, Sessions, Currency)
Given client unit = hours, when computing consumption, then scheduled durations (in minutes) plus applicable buffers are converted to hours with precision of 1 minute and used to forecast depletion. Given client unit = sessions, when computing consumption, then each scheduled session counts as 1 (plus no fractional buffer on count), while average_session_length and cadence are used only for projecting unscheduled sessions. Given client unit = currency, when computing consumption, then projected cost is derived using the engagement’s configured conversion rule (e.g., rate per hour or per session) applied to buffered durations; if no explicit rate is configured, use historical average cost from the lookback window. Given unit-specific rules, then projected_usage_by_week aggregates in the client’s configured unit and includes unit in the output. Given mixed inputs (e.g., sessions with explicit durations under a sessions-based plan), then the engine applies the plan’s unit rules deterministically and does not double-count duration and count.
Configurable Assumptions and Holiday Blackouts
Given assumptions target_cadence, session_length_variance_pct, holiday_calendar_id, and lookback_weeks are configured, when any assumption is changed, then the engine recomputes the forecast within 30 seconds and records change_reason = "assumption_change" with the changed keys. Given holiday_calendar_id is set, when projecting unscheduled sessions, then no projected sessions are placed on holiday dates defined by the calendar, and weekly totals adjust accordingly. Given assumptions are unset, then system defaults are applied (target_cadence and average_session_length from historical data per nightly recalibration; buffers default to 0 if not configured). Given changes to holiday calendars, then only weeks affected by added/removed holidays change in projected_usage_by_week, leaving other weeks unchanged.
Safety Buffer Application Rules
Given buffers percent_variance = P% and extra_minutes = M, when computing the effective duration for a session with base duration B minutes, then effective_duration_minutes = round(((B + M) * (1 + P/100))) and this value is used for consumption and projections. Given buffers are configured, then they apply consistently to both scheduled sessions and projected sessions derived from cadence. Given buffers are set to zero, then no buffer is applied and effective duration equals base duration. Given client-level buffer overrides exist, then client-level values take precedence over workspace defaults. Given currency-based forecasts, then percent_variance P% scales the projected cost; extra_minutes M impacts cost only when cost is duration-derived.
Forecast Outputs and Risk Tiering
Given a forecast is generated, then the output includes: depletion_date (ISO 8601 date), days_remaining (integer), projected_usage_by_week (array of {week_start_iso, amount, unit}), and risk_tier in {low, medium, high}. Given today’s date, when computing days_remaining, then days_remaining = floor((depletion_date - today) in calendar days) and is consistent with depletion_date. Given projected_usage_by_week, then the sum of weekly amounts up to depletion_date equals the total projected consumption to depletion within a tolerance of 0.5%. Given risk tiering, then risk_tier = high if depletion_date is within 7 days (or in the past), medium if within 8–21 days, and low if greater than 21 days from today. Given any recomputation, then outputs remain stable across repeated runs with the same inputs (deterministic).
Explainability Data and Auditability
Given a forecast is (re)computed, then explainability includes: assumptions_used (target_cadence, avg_session_length, buffers, lookback_weeks, holiday_calendar_id), drivers (list of included scheduled sessions with base_duration, buffer_applied, effective_duration, and unit/cost as applicable), and change_reason. Given any recomputation, then a new immutable forecast_version is created with timestamp, trigger_type (booking_change, nightly_recalibration, assumption_change), and before/after deltas of key metrics (depletion_date, days_remaining, risk_tier). Given explainability data, then the sum of driver contributions reconciles to the totals used in the forecast within a tolerance of 0.5%. Given an API request for a specific forecast_version, then the engine returns the exact snapshot used to produce the outputs, suitable for UI display and audit.
Low‑Credit Alerts & Notification Routing
"As a practitioner, I want timely low‑credit alerts for me and my clients so that we can act before service is interrupted."
Description

Introduce threshold-based alerts that trigger when a client’s forecasted balance crosses defined limits (e.g., hours remaining < X or days to depletion < Y). Support separate thresholds and templates for practitioner and client recipients, with channel routing for in‑app, email, and optional SMS. Include throttling, deduplication, quiet hours, and locale-aware templates to prevent spam and ensure timeliness. Alerts must include forecast context (remaining balance, predicted depletion date) and deep links to renewal actions. Provide per-client and global settings to tailor sensitivity and channels.

Acceptance Criteria
Practitioner Hours-Remaining Threshold Crossed
Given a global or per-client practitioner hours_remaining_threshold X is configured And a client’s forecasted remaining_hours changes from >= X to < X When the forecast engine detects the threshold crossing Then an alert to the practitioner is queued within 1 minute of detection And the alert payload includes remaining_hours with 2-decimal precision, predicted_depletion_date in practitioner timezone (ISO-8601), client_name, and a deep link to the client’s renewal action And no alert is sent if the client has alerts disabled or the engagement is paused And duplicate alerts for the same client and threshold type are suppressed for the practitioner for 24 hours
Client Days-to-Depletion Threshold Crossed
Given a client-specific or global days_to_depletion_threshold Y is configured for client recipients And the client’s forecasted days_to_depletion changes from >= Y to < Y When the forecast engine detects the threshold crossing Then an alert is queued to the client within 1 minute of detection using the client template And the alert includes remaining_balance_units, predicted_depletion_date in client timezone (ISO-8601), and a deep link to renewal And no alert is sent if the client has opted out of alerts or the channel is disabled And duplicate alerts for the same client and threshold type are suppressed for that client for 24 hours
Channel Routing and Optional SMS
Given per-recipient channel preferences are configured for in_app, email, and sms When a low-credit alert event is generated for a recipient Then the system delivers via all enabled channels for that recipient except sms unless sms is enabled and the phone number is verified and consented And an in-app notification appears in the notification center within 1 minute of event detection and links to the renewal action And an email is sent to the primary address using the correct channel template within 2 minutes of event detection And no sms is sent if sms is disabled, number is unverified, or consent is not present
Quiet Hours Deferral and Timezone Respect
Given quiet_hours are configured for a recipient and their timezone is known And an alertable threshold crossing is detected during that recipient’s quiet hours When the system evaluates send eligibility Then the alert is queued for delivery at the start of the next allowed window in the recipient’s timezone And if, at send time, the forecast no longer meets the threshold condition, the queued alert is discarded And no notifications are delivered during quiet hours for that recipient
Locale-Aware Templates and Fallback
Given a recipient has a locale configured When an alert is rendered Then date and number formats match the recipient’s locale And the localized template strings for that locale are used And if a locale-specific template is missing, the system falls back to the default locale template without rendering placeholders And dynamic fields (remaining balance, depletion date, recipient names, deep links) are populated without missing or null values
Per-Client Overrides vs Global Defaults
Given global default thresholds and channels are configured And a specific client has per-client thresholds and channel preferences set When alerts are evaluated for that client Then per-client settings override global defaults for thresholds, templates, and channels And unset per-client values inherit from global defaults And changes to per-client settings take effect for the next forecast evaluation cycle
Deduplication and Throttling Across Channels
Given an alertable event of type T is generated for recipient R When the system has already sent an alert of type T to R within the last 24 hours Then no additional alerts of type T are sent to R across any channel And retries caused by transient delivery failures do not create additional notifications in other channels And idempotency is enforced using a stable event key composed of client_id, recipient_id, threshold_type, and crossing_bucket
Suggested Top‑Up & One‑Tap Renewal
"As a client, I want a suggested top‑up with a one‑tap checkout so that I can quickly renew the right amount and avoid gaps in my engagement."
Description

Calculate an optimal top‑up amount that maintains continuity for a configurable coverage window (e.g., 4–8 weeks) based on forecasted consumption rate and package pricing. Present one‑tap renewal actions that generate a pre‑filled invoice/checkout with stored payment methods where available. On successful payment, automatically credit the ledger, refresh the forecast, and send confirmations; on failure, route a retry sequence and notify the practitioner. Provide guardrails for partial payments, taxes/fees, discounts, and multiple package options. Integrate with SoloPilot’s invoicing and payment processors via webhooks for idempotent, auditable updates.

Acceptance Criteria
Optimal Top‑Up Calculation for Coverage Window
Given a client with remaining balance and a configured coverage window in weeks And a forecasted consumption rate derived from cadence, booked sessions, and average session length And a selected package with defined unit price and purchase increments (e.g., packs of sessions or currency rounding rules) When the system computes the suggested top‑up amount Then the amount covers at least the forecasted consumption for the coverage window And the amount respects package increment and rounding rules And the projected coverage end date after top‑up is calculated and displayed And the calculation completes within 500 ms server‑side
Multi‑Package Option Suggestion and Default Selection
Given a client’s forecasted consumption for a configured coverage window And a package catalog with multiple eligible packages When generating top‑up suggestions Then the system presents ranked options (e.g., minimum coverage, optimal cost‑per‑unit, highest coverage within constraints) And each option displays package name, price, included credits/sessions, effective per‑unit rate, and predicted coverage end date And the default selection minimizes overage and selects the lowest effective per‑unit rate, breaking ties by lower total price And ineligible packages (eligibility rules, availability) are excluded from suggestions
One‑Tap Renewal with Pre‑Filled Invoice and Stored Payment Methods
Given invoicing is enabled and a payment processor is connected for the practitioner And the client has at least one stored payment method token (if available) When the user taps Renew on a suggested option Then a pre‑filled invoice is generated with line items for package, taxes/fees/discounts, and the suggested amount And a checkout/payment intent is initialized with the default stored method preselected And the user can switch to another stored method or add a new method before confirming And if no stored methods exist, the flow routes to a secure hosted checkout with pre‑filled invoice details And checkout supports SCA/3DS when required by the processor And tapping Renew to charge initiation completes within 2 seconds at the 95th percentile
Successful Payment: Ledger Credit, Forecast Refresh, Confirmations
Given a payment is confirmed as succeeded by the processor When the success webhook/event is received Then the client ledger is credited with purchased units/currency within 5 seconds And the Depletion Forecast is recalculated and updated in UI and API And the invoice status is marked Paid with transaction reference and processor fees recorded And confirmation messages are sent to the client and practitioner, and an in‑app activity log entry is created
Payment Failure: Retry Sequence and Practitioner Notification
Given a payment attempt fails or is declined When the failure webhook/event is received Then the invoice remains Unpaid with the failure reason captured and visible And a retry sequence is scheduled per processor best practices (e.g., d+1, d+3) unless disabled by configuration And the practitioner receives an in‑app alert and email including the next retry time and a manual collection link And after final retry exhaustion, the failure is logged with a summary and the suggestion remains available with updated recommendations
Taxes, Fees, Discounts, and Partial Payments Handling
Given applicable tax jurisdictions, configured platform/processing fees, and available discounts/coupons When computing the suggested top‑up and generating the invoice Then taxes are calculated per item and jurisdiction, itemized, and included in totals And discounts apply according to priority and stacking rules with an audit of applied discounts And platform/processing fees are itemized where applicable And if partial payments are allowed, split payments/payment plans proportionally credit the ledger as funds settle and display remaining balance clearly
Webhook Idempotency and Audit Logging
Given payment processor webhooks may be duplicated or arrive out of order When processing invoice and payment events Then idempotency is enforced using event IDs and business keys so ledger, invoice, and forecast updates occur exactly once And out‑of‑order events are detected and safely ignored or reconciled without double‑applying effects And every state change writes an immutable audit log including timestamp, actor/system, source event ID, and before/after values And a trace ID links the user action, invoice, payment intent, webhook events, and ledger entries via API
Client Self‑Serve Balance Portal
"As a client, I want an easy place to see my remaining credits and renew them so that I can stay on track without emailing back and forth."
Description

Offer a secure, branded portal where clients can view current balance, forecasted depletion date, usage history, and recommended top‑up options. Enable management of payment methods, downloading receipts, and initiating renewals from suggested options or custom amounts within allowed bounds. Access should be via authenticated client accounts or time‑bound magic links embedded in alerts. All actions must respect role-based permissions and be logged for audit and customer support.

Acceptance Criteria
Authenticated Client Portal Access (RBAC)
- Given a logged-in client user with active engagement, When they visit the Client Self-Serve Balance Portal, Then the portal loads over HTTPS and shows only data for their own account and workspace. - Given a logged-in user without the client role or lacking permission, When they attempt to access the portal, Then access is denied with HTTP 403 and no sensitive data is returned. - Given an inactive or suspended client account, When access is attempted via login or magic link, Then access is blocked with a clear message and support contact link. - Given any portal session, When the session is idle for 15 minutes (configurable), Then the user is signed out and redirected to the login screen, preserving the intended destination after re-authentication. - Given multi-tenant data isolation, When backend queries execute, Then they are scoped by workspace/tenant and never return cross-tenant records.
Time‑Bound Magic Link Access
- Given a magic link in an alert, When the link is opened within 15 minutes of issuance and before first use, Then the client is authenticated into the portal with scope limited to their account only. - Given a magic link is expired, revoked, or already used, When it is opened, Then show an Expired Link screen with options to request a new link or sign in; return HTTP 401 on API calls. - Given a magic link session, When device fingerprint or IP risk exceeds configured thresholds, Then require email OTP verification before granting access. - Given the workspace disables magic links or changes the client email, When previously issued links are opened, Then access is denied (401) and an audit entry is recorded. - Given a magic link session, When 30 minutes elapse without activity, Then the session expires and the user must re-authenticate.
Balance, Forecast, and Usage Display
- Given a client with a nonzero balance, When the portal loads, Then display current balance with currency, last updated timestamp in the client’s timezone, and the forecasted depletion date. - Given booked sessions, cadence, and average session length, When the forecast is computed, Then the depletion date matches the Depletion Forecast engine within ±1 minute and updates within 10 seconds of any relevant change. - Given usage history exists, When viewing the Usage tab, Then itemized entries show date, session length, debit/credit, description, and running balance with pagination and CSV export. - Given a low-credit threshold is configured, When the balance is at or below the threshold, Then show a non-blocking low-balance banner with a link to Top-Up options. - Given no usage exists, When opening Usage, Then show an empty state message without errors.
Suggested Top‑Up and One‑Tap Renewal
- Given a low-balance state, When Top-Up options are opened, Then show three suggested amounts to cover approximately 2, 4, and 8 weeks of forecasted usage (rounded to the workspace billing unit) plus a Custom Amount option. - Given workspace constraints for min, max, and increment, When a custom amount is entered, Then inline validation enforces bounds and increments and disables Pay until valid. - Given a default payment method exists, When the client selects a suggested or valid custom amount and taps Pay, Then the charge is processed in one step, balance updates, forecast recalculates, and a success receipt is displayed within 5 seconds of processor confirmation. - Given a payment attempt fails (e.g., SCA/3DS required, insufficient funds, network error), When Pay is tapped, Then the client sees a specific error, can retry or choose another method, and no duplicate charges occur. - Given an alert deep link includes a suggested amount parameter, When the portal opens from that link, Then the corresponding suggestion is preselected.
Payment Methods Management
- Given the client opens Payment Methods, When adding a card, Then details are validated (Luhn, expiry, CVC), tokenized client-side, and transmitted over TLS 1.2+; raw PAN/CVC are not stored on SoloPilot servers. - Given multiple methods exist, When the client sets a default, Then the selected method becomes default for one‑tap payments and is displayed as such thereafter. - Given a payment method has pending charges, When the client attempts to remove it, Then removal is blocked with an explanation; otherwise removal succeeds and a new default is assigned if needed. - Given SCA/3DS is required by the processor or region, When adding or charging a card, Then the flow supports necessary challenges and reports clear outcomes. - Given ACH is enabled by the workspace, When a client adds a bank account, Then micro-deposit or instant verification is supported and the method shows Pending until verified.
Receipts History and Download
- Given past top-ups or renewals exist, When the client opens Receipts, Then receipts list with date, amount, tax, payment method, and status and support search and date range filters. - Given a receipt is selected, When Download is tapped, Then a branded PDF downloads containing workspace logo, legal entity and address, tax IDs, line items, unique receipt/invoice number, and currency. - Given the client taps Send via Email on a receipt, When the action completes, Then the receipt is emailed to the client’s address and any CC contacts and the action is confirmed in‑app. - Given no receipts exist, When Receipts is opened, Then show an empty state with a link to Top-Up.
Audit Logging and Support Traceability
- Given any portal action (view balance, add/remove payment method, start/complete/failed payment, receipt download, login via magic link), When it occurs, Then an immutable audit record is captured with actor id, timestamp (UTC), IP, user agent, action, resource id, workspace id, and outcome. - Given a support agent with proper role, When searching audit logs by client id or transaction id, Then results return within 2 seconds and include correlation ids for cross-system tracing. - Given sensitive data in logs, When logs are viewed by staff, Then PANs, CVCs, and email addresses are masked/redacted and access is enforced by RBAC.
Capacity Planning Overview
"As a practitioner, I want a consolidated view of which clients will deplete soon so that I can plan my schedule and outreach to maintain stable workload and cash flow."
Description

Provide a practitioner dashboard that aggregates forecasts across all active clients, highlighting those nearing depletion and estimating weekly utilization and revenue continuity. Include sorting/filtering by depletion date, risk tier, package type, and account size, plus bulk actions to send nudges. Visualize upcoming booked sessions versus forecasted coverage to help plan availability and marketing. Support CSV export and API access for reporting.

Acceptance Criteria
Capacity Overview Aggregates Active Client Forecasts
Given a practitioner with at least 1 active client with a balance and forecast When they open the Capacity Planning Overview Then the dashboard lists one row per active client with columns: Client Name, Package Type, Account Size (remaining credits/hours with unit), Forecasted Depletion Date, Risk Tier, Booked Sessions (next 28 days), Avg Session Length, Suggested Top-up Amount And the list is paginated 50 rows per page with total count shown And data freshness is <= 15 minutes from last forecast run And initial load time is <= 2.0 seconds for up to 2,000 clients
Multi-criteria Sorting and Filtering
Given the capacity list is loaded When the user sorts by Depletion Date ascending or descending Then rows reorder accordingly and the active sort indicator is visible When sorting by Risk Tier Then order is High, Medium, Low, Unknown When sorting by Account Size Then numeric sort is applied with Unknown last When applying filters by Depletion Date range, Risk Tier(s), Package Type(s), and Account Size range Then only matching clients are shown and applied filters display as chips And multiple filters can be combined and persist in the URL so refresh preserves them And Clear All resets to default and shows all clients And when no results match Then an empty-state message and a Reset Filters action appear
Depletion Risk Highlighting and Thresholds
Given default risk rules are active When a client's forecasted depletion date is within 0–7 days or remaining balance < 1 session-hour equivalent Then Risk Tier = High and the row shows a red risk badge with WCAG AA contrast When depletion is within 8–21 days Then Risk Tier = Medium and the row shows an amber badge When depletion is > 21 days Then Risk Tier = Low and the row shows a green badge And hovering the risk badge shows a tooltip with the rule applied and the projected depletion date
Weekly Utilization and Revenue Continuity Estimates
Given the practitioner's weekly capacity is defined and bookings exist When the dashboard loads Then it displays utilization for the next 1, 2, and 4 weeks = booked hours ÷ weekly capacity, rounded to 1 decimal place And it displays Revenue Continuity for the next 4 weeks = percentage of booked sessions covered by current balances/top-ups as forecasted, rounded to 1 decimal place And hovering each metric shows numerator and denominator details And metrics update within 5 minutes of booking or balance changes
Coverage vs Booked Sessions Visualization
Given upcoming booked sessions and forecast coverage data When the user views the visualization Then a weekly stacked bar appears for the next 8 weeks with segments: Covered Hours, At-Risk Hours, Uncovered Hours And tooltips show week date range, hours per segment, and top at-risk clients count And clicking a bar filters the list to clients contributing to Uncovered Hours for that week And all times respect the practitioner's timezone and mobile screens support horizontal scroll And the chart renders in <= 500 ms for up to 8 weeks of data
Bulk Nudge Actions to At-Risk Clients
Given a filtered list of clients When the user selects 1–200 clients and clicks Send Nudge Then a confirmation modal shows recipient counts, template preview with variables (client name, remaining balance, projected depletion date, suggested top-up amount), and channel selection (email) And on Send the system dispatches messages, de-duplicating per client per 24 hours, respecting opt-outs, and returns a per-client result summary (sent, skipped, failed) And a one-tap renewal link is included using the client's package, prefilled with suggested top-up amount = amount to cover the next 4 weeks of booked sessions, rounded to the nearest package unit And an audit log entry is created per client with timestamp, sender, channel, and message id
Data Export and Reporting API
Given the current filtered/sorted view When the user clicks Export CSV Then a CSV is generated within 10 seconds with UTF-8 encoding and headers, including one row per client and columns: client_id, client_name, package_type, account_size_value, account_size_unit, forecasted_depletion_date (ISO 8601), risk_tier, booked_sessions_next_28d, avg_session_length_minutes, suggested_top_up_amount, at_risk_week_numbers And the CSV reflects the same filters and sort order as the UI and uses the practitioner's timezone for date grouping And exports up to 50,000 rows; if larger, the user is prompted to refine filters And an authenticated Reporting API endpoint GET /v1/capacity-overview supports the same filters and sorts via query params, returns JSON with pagination (limit, offset, total), responds in <= 1.5 seconds for up to 2,000 records, and enforces rate limits (60 req/min per key) with proper error codes (400/401/429/500)

Overage Autopilot

Automatically bills overages the moment a session exceeds the plan—using your rules for grace minutes, rounding, and rate tiers. Adds a clear line item to the session’s invoice and sends a transparent breakdown to the client. Recovers revenue you’d otherwise miss and shortens days sales outstanding without manual review.

Requirements

Overage Rules Engine
"As an independent consultant, I want overage charges to be calculated automatically according to my grace minutes, rounding, and tiered rates so that I recover extra time revenue without manual math and policy checks."
Description

Implement a deterministic, rule-based calculation engine that converts session duration beyond plan inclusion into billable overage charges using configurable parameters: grace minutes, rounding increments, minimum billable units, tiered rates, per-session/per-period caps, and service-specific exceptions. The engine must operate in real time and batch (for retroactive sessions), support time zone and daylight-saving edge cases, respect client currency and lock exchange rates at invoice creation, and return a structured breakdown (inputs, rule version, steps, outputs) for downstream invoice rendering and client communications. It must be idempotent, versioned with effective dates, and resilient to session edits, allowing safe recomputation and rollback while preserving an auditable history.

Acceptance Criteria
Grace Minutes, Rounding, and Minimum Units
Given a plan includes N minutes and a session duration is D minutes and grace minutes G, rounding increment R, and minimum billable units M are configured When D - N <= G Then billable_overage_minutes = 0 and overage_amount = 0 Given the same configuration When D - N > G Then raw_overage = D - N - G And rounded_overage = ceil(raw_overage / R) * R And billable_overage_minutes = max(rounded_overage, M) And repeated runs with identical inputs return identical billable_overage_minutes and overage_amount
Tiered Rates and Service-Specific Exceptions
Given tiered rates are configured with thresholds, rates, and a calculation_mode (stepped or flat) and a service S is provided When billable_overage_minutes = X is computed Then if calculation_mode = stepped, the engine applies each tier rate to minutes within that tier and sums the amounts And if calculation_mode = flat, the engine applies the rate of the tier that contains X to all X minutes And if service S is marked exempt from overage, overage_amount = 0 regardless of X And if service S has its own rule set, the engine selects and applies that rule set for the calculation
Overage Caps: Per-Session and Per-Period
Given per_session_cap_minutes C_s and/or per_session_cap_amount A_s and per_period_cap_minutes C_p and/or per_period_cap_amount A_p for period P and prior billed minutes M_p and prior billed amount A_p_prev in P are known When preliminary billable_overage_minutes = X and preliminary overage_amount = A_pre are computed Then session_billed_minutes = min(X, C_s) if C_s is set, otherwise X And session_billed_amount = min(A_pre, A_s) if A_s is set, otherwise A_pre And cumulative_period_minutes = min(M_p + session_billed_minutes, C_p) if C_p is set, otherwise M_p + session_billed_minutes And cumulative_period_amount = min(A_p_prev + session_billed_amount, A_p) if A_p is set, otherwise A_p_prev + session_billed_amount And if any cap is applied, the breakdown flags cap_applied = true with cap_type = session or period and cap_dimension = minutes or amount
Real-Time and Batch Processing with Idempotency and Edits/Audit
Given a session end event occurs and an active rule version applies When the real-time calculation is triggered multiple times with identical inputs Then exactly one persisted calculation record exists per session occurrence and all invocations return the same calculation_id, billable_overage_minutes, and overage_amount Given a batch job runs for a date range containing sessions with and without prior calculations When the engine processes the batch Then sessions with unchanged inputs yield identical results without duplicate records and sessions with changed inputs are recomputed Given a session is edited (start, end, duration, service, or plan association) When recomputation runs Then the engine selects the rule version by effective-date logic for the session’s service/timestamp and creates a new calculation record linking to the superseded record And a rollback operation can restore the superseded record without data loss And the audit log contains who, when, what changed, prior values, new values, and calculation linkage
Structured Breakdown Output for Invoicing and Communications
Given a calculation completes When the engine returns its result Then the payload includes inputs (plan_included_minutes, session_start_utc, session_end_utc, session_timezone, duration_minutes, grace_minutes, rounding_increment, minimum_units, tier_config, caps_config, service_id, currency, exchange_rate_source) and rule_version (id, effective_start, effective_end) And the payload includes steps as an ordered list with name, inputs, intermediate_values, and outputs per step And the payload includes outputs (billable_overage_minutes, rates_applied, overage_amount, caps_applied, tier_hits, calculation_id) And the payload includes schema_version and content_hash for integrity And the payload fields are sufficient to render a clear invoice line item and client-facing breakdown without additional computation
Time Zone and Daylight Saving Handling
Given a session has a designated IANA time zone TZ and starts/ends around DST transitions When duration is computed Then duration reflects true elapsed wall-clock time in TZ for both spring-forward and fall-back days And sessions spanning midnight, month boundaries, leap days, and timezone changes compute correct duration and do not double-charge or miss minutes And the breakdown echoes TZ and includes both local and UTC timestamps used in the calculation
Currency and Exchange Rate Lock at Invoice Creation
Given a client currency CCY and an organization base currency BCY and an exchange rate provider are configured When a session’s overage is invoiced for the first time Then the engine locks the exchange rate with rate, source, and timestamp at invoice creation and applies currency-appropriate precision to rounding And subsequent recomputations for the same invoice reuse the locked rate even if market rates change And for sessions not yet invoiced, the engine uses the configured rate lookup policy at calculation time And the breakdown includes currency codes, rate used, rounding precision, and rate timestamp
Real-time Overage Detection & Trigger
"As a coach, I want SoloPilot to detect when a session runs long and instantly apply my overage rules so that the correct charge appears on the invoice without me intervening."
Description

Detect when an active or logged session exceeds the included plan minutes and immediately trigger the overage calculation and invoice update. Integrate with SoloPilot’s scheduling/session tracker and calendar integrations to ingest actual start/stop times, handle missed check-in/out with heuristics and edits, and support offline/retrospective entry. Ensure concurrency safety to prevent duplicate triggers, implement retry and dead-letter queues for failures, and expose webhook events for external systems. Target sub-5-second latency from threshold crossing to invoice update and record a trace ID to correlate detection, calculation, and notification flows.

Acceptance Criteria
Active session real-time overage trigger
Given an active session linked to a plan with includedMinutes=60 and graceMinutes=5 And an accurate session start time is recorded When the session’s elapsed duration exceeds 65:00 Then the system triggers overage detection and completes the invoice update within 5,000 ms of the threshold crossing timestamp And exactly one overage line item is present on the session’s invoice And the line item includes metadata: type=overage, sessionId, overageMinutes, rateApplied, amount, traceId And the same traceId is present in detection, calculation, and invoice update logs/records
Grace minutes and rounding rules applied
Given a plan with graceMinutes and roundingIncrement and roundingMode settings (e.g., graceMinutes=5, roundingIncrement=15, roundingMode=ceil) When actualDurationMinutes - includedMinutes - graceMinutes > 0 Then overageMinutes = roundingMode((actualDurationMinutes - includedMinutes - graceMinutes) / roundingIncrement) * roundingIncrement And if actualDurationMinutes ≤ includedMinutes + graceMinutes, overageMinutes = 0 and no overage line item is added And the computed overageMinutes value is persisted on the invoice line item
Rate tier selection for overage pricing
Given rate tiers are configured for overage pricing (e.g., 0–30 min @ $2.00/min, >30 min @ $1.50/min) When overageMinutes are computed Then the amount is calculated by applying the tiers in order with correct breakpoints and quantities per tier And the invoice line item displays unit rate(s), tier breakdown (if multi-tier applied), currency, and total amount matching the calculation And monetary precision follows the account currency settings
Concurrency safety and idempotency
Given multiple workers or services receive duplicate threshold-crossing events for the same session When they attempt to process overage detection and invoice update concurrently Then exactly one calculation is committed and exactly one overage line item is created or updated for the session occurrence And subsequent duplicate attempts are no-ops, resolved via an idempotency key (e.g., sessionId + sessionDate + planId) and return the existing result And no duplicate webhook notifications are emitted for the same traceId
Retry and dead-letter on downstream failure
Given the invoice store/service returns a transient 5xx error or times out When the system attempts to update the invoice with the overage line item Then the operation is retried using exponential backoff up to maxRetries (default 3), with each attempt correlated by traceId And if retries are exhausted, the message is placed on a dead-letter queue with failure reason and payload snapshot And no partial or duplicated line item is visible on the invoice And when a DLQ message is replayed after recovery, exactly one correct overage line item is created and the DLQ entry is cleared
Webhook events emission and payload integrity
Given an overage is detected and invoiced When events are emitted Then overage.detected is emitted upon detection and overage.invoiced upon invoice update And each payload includes: eventType, sessionId, clientId, planId, overageMinutes, rateApplied or tierSummary, amount, invoiceId, traceId, occurredAt And events are signed with the account webhook secret and include unique eventId and idempotencyKey And delivery is at-least-once with retry on non-2xx or timeout up to configured limits, preserving per-session ordering to each endpoint
Offline entry and missed check-in/out handling with edits
Given a session is entered retrospectively with explicit start and stop times When the session is saved Then overage detection runs immediately and updates the invoice per plan rules And Given a session has a start time but missing stop time at scheduled end When a connected calendar provides a meeting end time or a user later enters the stop time Then the best available end time (calendar > scheduled) is used to infer duration, the line item is flagged as inferred, and overage is triggered if applicable And When session times are edited after invoicing, Then the system recalculates and replaces the prior overage line item with a corrected one, preserving an audit trail linked by traceId
Invoice Line Item Injection
"As a freelancer, I want overages to appear as a clear line item with a detailed breakdown on the invoice so that clients understand the charge and pay faster."
Description

Automatically append a transparent overage line item to the session’s invoice with a human-readable breakdown (included minutes, grace applied, rounded billable minutes, rate tier(s), unit price, subtotal, taxes) and structured metadata (accounting code, tax category, rule version, trace ID). If the invoice has already been issued or paid, create an adjustment invoice or schedule the overage for the next invoice per account settings. Enforce idempotency keys to avoid duplicates, support recalculation on session edits with diffed adjustments, and integrate with payment gateways and accounting exports (e.g., Stripe, QuickBooks/Xero). Provide an invoice preview mode prior to sending and respect multi-currency and tax rules.

Acceptance Criteria
Core overage calculation and line-item breakdown
Given account settings: included_minutes=60, grace_minutes=5, rounding_increment=15 minutes, rate_tiers=[61-120:$2 per minute], and an open invoice for the session And a completed session with actual_duration=82 minutes When Overage Autopilot runs for the session Then exactly one overage line item is appended to the open invoice directly after the session line And computed billable_overage_minutes = ceil(max(82-60-5,0)/15)*15 = 30 And unit_price = 2.00 per minute and quantity_minutes = 30 and line_subtotal = 60.00 And the line description includes: "Included minutes: 60", "Grace applied: 5", "Rounded billable minutes: 30", "Rate tier: 61-120 @ $2/min", "Unit price: $2.00", "Subtotal: $60.00" And the line shows calculated taxes per the client's tax rules (tax amount present or 0.00) And the line is visible in invoice preview
Idempotent injection prevents duplicate overage lines
Given an idempotency_key derived from session_id, invoice_id, and rule_version And a prior successful overage injection exists for the same key When the system receives duplicate events or retries for the same session overage Then at most one overage line item exists on the invoice for that key And the operation returns the existing line_item_id without creating a duplicate And concurrent attempts across workers result in a single line item (no duplicates) And if any input changes (duration, rules, currency, tax), a new idempotency_key is generated and a new diff flow is triggered instead of duplicating the prior line
Recalculation and diff adjustment on session edits
Given an existing overage line item with trace_id=T1 for a session initially at 82 minutes resulting in 30 billable overage minutes at $2.00/min (subtotal $60.00) And the session is edited to 97 minutes with the same rules When Overage Autopilot recalculates Then new billable_overage_minutes = ceil(max(97-60-5,0)/15)*15 = 45 And a new adjustment line item is created with trace_id=T1, type=adjustment, delta_minutes=+15, delta_subtotal=+30.00 And the adjustment line description references the prior line (trace_id T1) and shows before vs after minutes and amounts And if the recalculation results in a negative delta, the adjustment line subtotal is negative and reduces the invoice total And no original line items are deleted; history remains auditable
Issued/paid invoice handling per account settings
Given the session’s invoice status is Issued (unpaid) and account setting is "Create adjustment invoice immediately" When an overage is detected or recalculated Then an adjustment invoice is created in Draft with a single overage (or adjustment) line in the correct currency and tax category, linked to the original invoice And if "Require preview before send" is enabled, the adjustment invoice stays in Draft until explicitly sent; otherwise it is issued automatically And if the original invoice status is Paid and account setting is "Queue for next invoice", the overage is scheduled as a pending charge and appears on the next invoice generated for the client with a reference to the originating session
Invoice preview mode before sending
Given a user requests Preview for an invoice containing an overage line When the preview is generated Then the preview shows the overage line with included minutes, grace applied, rounded billable minutes, rate tier(s), unit price, subtotal, taxes, and total impact And currency symbols and formatting match the client currency and locale And no changes are persisted to the live invoice until the user selects Send or Save Draft And selecting Send issues the invoice and dispatches the client notification; selecting Cancel or Close discards the preview without persisting any new line items
Multi-currency and tax compliance for overage line
Given account base currency is USD, client currency is EUR, and tax rules require 21% VAT with tax_category="Services.EU.VAT21" And the account’s FX policy is "Invoice-date rate" When an overage line is created on invoice date 2025-09-22 Then the unit price and subtotal are converted to EUR using the invoice-date FX rate captured on 2025-09-22 and rounded to 2 decimals And the VAT amount equals 21% of the taxable subtotal in EUR, rounded per tax rules And the line item metadata.tax_category is "Services.EU.VAT21" and the invoice totals (net, tax, gross) reconcile to within 0.01 EUR of system calculations
Gateway and accounting export integration for overage lines
Given an invoice with an overage line is finalized When syncing to Stripe Then a corresponding Stripe invoice item is created or updated with matching amount, currency, description text, and metadata (accounting_code, tax_category, rule_version, trace_id) And the Stripe PaymentIntent amount reflects the updated invoice total When exporting to QuickBooks or Xero Then the overage line maps to the configured income account via accounting_code and to the correct tax rate, with no discrepancy greater than 0.01 in any line or total And the export includes the metadata fields where supported and preserves the trace_id for audit
Client Overage Notification Templates
"As a therapist, I want clients to receive a clear, automated breakdown of any overage so that I don’t have to manually justify extended sessions."
Description

Send an automatic, branded notification to the client portal and email when an overage is applied, including a concise explanation of grace minutes used, rounding applied, billable minutes, rate tier, and policy link. Provide customizable templates with localization, time zone awareness, merge fields, and delivery scheduling (on trigger, on invoice, or consolidated digest). Include a configurable dispute window with a self-service link, opt-out controls where legally required, and delivery logs with open/click tracking. Ensure accessibility (plain-text and HTML), rate limiting, and compliance with regional communication regulations.

Acceptance Criteria
Immediate Overage Notification Content and Dual-Channel Delivery
Given a session exceeds the client’s plan and an overage is applied When notification delivery is set to On trigger Then a portal notification is created and an email is sent to the client’s primary email within 60 seconds And the message includes: grace minutes used, rounding rule applied, billable minutes, applied rate tier name and rate, session date/time, session ID/reference, invoice reference (if available), and a policy link URL And the email uses the organization’s configured branding (logo, colors, sender name) And if the email hard-bounces, the portal notification persists and the bounce is logged with provider code and reason
Template Customization, Merge Fields, and Localization
Given an admin edits the Client Overage Notification templates When they insert supported merge fields (e.g., {{client.first_name}}, {{session.start_time}}, {{overage.billable_minutes}}, {{rate.tier_name}}, {{policy.url}}) Then a real-time preview renders the merged content using sample data for the selected locale And saving validates tokens; unsupported merge fields are rejected with a clear error listing invalid tokens And the admin can create locale-specific variants; if a client’s locale has no variant, the default template is used And dates, times, and numbers render per the selected locale’s formats in both email and portal
Time Zone-Aware Timestamps and Rounding Display
Given the client profile has a time zone set When an overage notification is generated Then all displayed timestamps (session start/end, grace window end, invoice posting time if referenced) render in the client’s time zone And an explicit time zone abbreviation/offset is included And if no client time zone exists, the organization’s default is used And daylight saving transitions are reflected correctly And the rounding explanation references client-local times (e.g., “rounded up to 45 min at HH:mm local”)
Delivery Scheduling: On Trigger, On Invoice, and Consolidated Digest
Given delivery scheduling is configured to On trigger, On invoice, or Consolidated digest When overages occur Then On trigger sends within 60 seconds of overage application And On invoice sends within 60 seconds of an invoice posting that includes the overage line item And Consolidated digest sends a single summary at the configured cadence (e.g., daily at 18:00 client local) aggregating all overages since the previous digest And the digest lists each session with grace minutes used, rounding applied, billable minutes, rate tier, and policy link And duplicate notifications for the same overage across schedules are prevented And the client portal shows a corresponding message with the same content for each email or digest
Configurable Dispute Window and Self-Service Submission
Given a dispute window duration and self-service link are configured When a client receives an overage notification Then the message includes a dispute link and the deadline timestamp in the client’s time zone And submitting a dispute within the window captures reason and optional attachment, associates it with the session/invoice, and returns a confirmation And submissions after the window are rejected with an explanatory message And a dispute event is logged with timestamp, client, session/invoice references, and correlation ID
Legal Opt-Out Controls and Regional Compliance
Given the client’s region requires opt-out for overage notifications or the organization has enabled opt-out When an email notification is sent Then the footer includes a link to manage overage notification preferences And if the client opts out, future overage emails are suppressed while portal notifications continue And suppression and consent changes are timestamped, auditable, and exportable And region-required legal elements (e.g., company address, legal entity) are included And tracking pixels and certain links are suppressed or consent-gated where required by regional regulations
Accessible Formats, Tracking, Delivery Logs, and Rate Limiting
Given overage notifications are generated When emails are sent Then messages are multipart/alternative with accessible HTML and plain-text versions And the HTML version uses semantic structure, sufficient color contrast, descriptive link text, and alt text for images And open and click events returned by the provider are recorded and associated with the notification And delivery logs capture per-notification: channel, recipient, send timestamp, delivery status, provider message ID, open/click events, and error details if any And configurable rate limits per account are enforced; excess messages are queued with retry/backoff and throttling is visible in logs
Policy Configuration & Overrides
"As a practice owner, I want to configure default and client-specific overage policies with effective dates and a simulator so that billing remains consistent, fair, and defensible."
Description

Offer an admin UI to define global defaults, plan-level policies, service-type rules, and client-specific overrides for grace minutes, rounding increments, minimums, tier thresholds and rates, per-session/per-period caps, exclusions, and consumption order with time banks/retainers. Support draft/test mode with a what-if simulator against historical sessions, effective-date versioning, permissions/roles for who can view/edit, input validation to prevent conflicting rules, and import/export of policy sets. Persist an immutable audit log of changes with before/after values and user attribution for compliance and traceability.

Acceptance Criteria
Policy Hierarchy & Precedence Resolution
Given a global default D, a plan-level P (Plan Alpha), a service-type S (Coaching), and a client-specific C (Client Acme) When a Coaching session for Client Acme under Plan Alpha is reconciled Then the applied policy is C and the session stores applied_policy_id=C and source_tier=Client Given D, P (Plan Alpha), S (Coaching), and no client-specific override When a Coaching session for Client Beta under Plan Alpha is reconciled Then the applied policy is S and the session stores applied_policy_id=S and source_tier=Service Given D and P (Plan Alpha) and no service or client overrides When a Therapy session for Client Gamma under Plan Alpha is reconciled Then the applied policy is P and the session stores applied_policy_id=P and source_tier=Plan Given only D When an Advisory session for Client Delta without a matching plan/service rule is reconciled Then the applied policy is D and the session stores applied_policy_id=D and source_tier=Global
Policy Elements Configuration & Validation
Given grace_minutes=5, rounding_increment=15, minimum_billable_minutes=30 When saving the policy Then values persist and subsequent calculations use these values Given rounding_increment=7 (not in [1,5,6,10,15,30,60]) When attempting to save Then save is blocked and the UI shows "Invalid rounding increment" and no changes persist Given tier_thresholds=[60,120] and tier_rates=[100,80] (USD) When saving Then thresholds must be strictly ascending and rates>0; on violation, show specific errors and block save Given per_session_cap_minutes=20 and minimum_billable_minutes=30 When saving Then save is blocked with "Cap cannot be less than minimum billable" and no changes persist Given exclusions include service_code=XYZ that does not exist When saving Then save is blocked with "Unknown service code: XYZ" and no changes persist Given two rounding rules defined at the same scope When saving Then the system blocks with "Conflicting rules at same scope" and highlights duplicates
Consumption Order with Time Banks/Retainers
Given time_bank_balance=30m, plan_included_minutes=60m, consumption_order=[TimeBank,Plan,Overage] When a 90-minute Coaching session is reconciled Then 30m are deducted from time bank, 60m from plan, 0m billed as overage, and balances reflect the deductions Given time_bank_balance=30m, plan_included_minutes=60m, consumption_order=[Plan,TimeBank,Overage] When a 90-minute Coaching session is reconciled Then 60m are deducted from plan, 30m from time bank, 0m billed as overage Given time_bank_balance=30m, plan_included_minutes=60m, consumption_order=[TimeBank,Plan,Overage], overage_rate=$2/min, per_session_overage_cap=$60 When a 120-minute Coaching session is reconciled Then 30m bank + 60m plan + 30m overage are applied and overage_charge=$60 due to cap Given exclusions include service_code=INTERNAL_MEETING When a session with service_code=INTERNAL_MEETING is reconciled Then no minutes are consumed from any source and no overage is billed
Draft/Test Mode & What-If Simulator
Given a draft policy set V2 with future effective_date=2025-11-01 When running the simulator for client=Acme across 2025-06-01..2025-06-30 Then for each session the report shows current_billed_minutes, current_amount, proposed_billed_minutes(V2), proposed_amount(V2), and delta, and no invoices or balances are modified Given a completed simulator run When exporting results Then a CSV is downloaded including columns [session_id,date,client,service,policy_version_current,policy_version_proposed,amount_current,amount_proposed,delta,source_tier_proposed] Given a simulator run over 1,000 sessions When executed Then initial results start streaming within 5 seconds and the run completes within 60 seconds or shows a progress indicator until completion Given draft mode remains inactive When live billing occurs before 2025-11-01 Then policy V1 continues to be used and V2 is never applied to live calculations
Effective-Date Versioning
Given active policy set V1 effective_date=2025-01-01 When creating V2 with effective_date=2025-11-01 Then V2 status=Scheduled and no overlap with V1 is allowed Given an attempt to create V2 with effective_date=2025-06-01 overlapping V1 When saving Then save is blocked with "Effective date overlaps an existing version" and no changes persist Given policy versions V1 (2025-01-01) and V2 (2025-11-01) When reconciling a session dated 2025-10-31 Then V1 is applied; when reconciling a session dated 2025-11-02, V2 is applied Given historical versions When viewing V1 details Then fields are read-only and labeled Immutable to prevent retroactive edits
Permissions & Roles (View/Edit)
Given roles {Admin, BillingManager, Viewer} When an Admin creates/edits/saves a policy set Then the operation succeeds and the audit log records user_id, action, and before/after values Given a BillingManager attempts to edit a policy set When clicking Save Then the Save control is disabled in UI and any direct API call returns 403 Forbidden and no changes persist Given a Viewer navigates to the policy UI When the page loads Then all form controls are read-only and the simulator Run button is hidden Given an unauthenticated user When calling any policy API endpoint Then the response is 401 Unauthorized
Import/Export & Immutable Audit Logging
Given an Admin exports policy set V1 When exporting Then a JSON file downloads containing metadata (name, version_id, effective_date), rule definitions, and a SHA-256 checksum; no PII is included Given an Admin imports a policy JSON file When validating Then schema, checksums, and rule conflicts are validated and on success a new Draft version is created; on failure, field-level errors are displayed and import is aborted Given an import file with a version_id that already exists When importing Then the system generates a new version_id and preserves referential integrity Given any policy change (create, update, delete, import, export) When the action completes Then an immutable audit log entry is written with timestamp(UTC), user_id, action, entity_id, before_values, after_values, and reason; entries cannot be edited or deleted by any role Given an attempt to delete an audit log entry via API When executed Then the API responds 405 Method Not Allowed and no change occurs
Overage Analytics & Audit Trail
"As an operations lead, I want transparent analytics and detailed audit logs for every overage so that I can prove accuracy, resolve disputes quickly, and optimize policies over time."
Description

Provide dashboards and exports that quantify recovered overage revenue, impact on days sales outstanding, exception rates (caps reached, adjustments), top clients/services by overage, and dispute outcomes. For each overage, store a full audit trail including input data, rule version, calculation steps, responsible user for policy changes, and links to invoice and payments for reconciliation. Support filters by time range, client, service, and policy version; CSV/JSON export; alerts for anomalies; and data retention controls aligned with privacy requirements and SOC 2 audit practices.

Acceptance Criteria
Overage Revenue & DSO Dashboard Metrics
- Given invoices with overage line items and payments exist in the last 90 days, When the user loads the Overage Analytics dashboard with default filters (last 90 days, all clients/services, all policy versions) in the workspace timezone, Then Recovered Overage Revenue equals the sum of non-voided, non-written-off invoice line items where line_type = "Overage" and invoice_issue_date is within the range, displayed in the workspace currency. - Given payments are matched to invoices, When DSO metrics render, Then the dashboard shows: DSO_overage = weighted average of (payment_date - invoice_issue_date) in days for invoices with ≥1 overage line in the filter; DSO_non_overage = weighted average for invoices without any overage lines in the same period; DSO_impact = DSO_non_overage - DSO_overage. - Given overage events exist, When Exception Rate renders, Then it equals (count of overage events with cap reached OR manual adjustment OR dispute raised) / (total overage events in filter) rounded to 1 decimal place, with per-type subtotals. - Given multiple clients and services exist, When Top Clients/Services by Overage renders, Then the top 10 are ordered by total recovered overage descending (ties broken alphabetically) and each item is clickable to apply a filter. - Given disputes exist, When Dispute Outcomes renders, Then counts and percentages by outcome (Open, Won, Lost, Withdrawn) match dispute records linked to overage events in the filter. - Given ≤100k overage events in range, When the dashboard loads, Then all tiles render in ≤3s P95 and ≤6s P99.
Filter Controls and Query Accuracy
- Given a user selects a time range (start 00:00:00 inclusive, end 23:59:59 inclusive) in the workspace timezone, and selects one or more clients, services, and a policy version, When Apply is clicked, Then all dashboard tiles and tables reflect the intersection across dimensions (AND) and union within a dimension (OR) and display a filter summary. - Given multi-select filters are used, When the user selects multiple clients and services, Then results include records where client ∈ selected clients AND service ∈ selected services. - Given the user resets filters, When Reset is clicked, Then filters revert to defaults (last 30 days, all clients, all services, all policy versions) and the view refreshes. - Given identical filters are applied, When the user compares on-screen totals to an export produced with the same filters, Then counts and sums match exactly. - Given a filter would match ≤1M rows, When the query runs, Then the initial page responds in ≤2s P95, and full result sets are paginated for UI while exports are handled via background jobs. - Given a user returns to the dashboard, When they have not changed filters since the last session, Then their last-used filters persist for that user only.
Per-Overage Audit Trail Completeness & Immutability
- Given an overage event is generated, When its audit record is opened, Then it includes: overage_event_id, session_id, client_id, service_id, timestamps (UTC and workspace TZ), input parameters (scheduled duration, actual duration, grace minutes, rounding rule and precision, rate tier id and rates, caps/limits), policy_version_id, calculation steps (pre-grace duration, billable duration after rounding, unit price, quantity, line total), and links to invoice_id, payment_ids (if any), and dispute_id (if any). - Given policy settings change, When a change could affect calculations, Then the audit shows responsible_user_id, timestamp, previous_value, new_value, and the policy version hash used in the calculation. - Given an audit record exists, When an admin attempts to edit fields, Then direct edits are blocked; only an append-only correction entry can be added referencing the original, leaving the original immutable. - Given daily integrity checks run, When the audit log is validated, Then each record's integrity hash matches its content and predecessor hash; failures create an anomaly alert entry. - Given a user without AuditTrail.View permission attempts access, When they request an audit record, Then access is denied with HTTP 403 and the attempt is logged with user, timestamp, and IP.
CSV and JSON Export Fidelity and Limits
- Given any filter combination is applied, When the user exports to CSV and JSON, Then exported row count equals the number of matching audit records; CSV is UTF-8 with LF newlines, quoted fields, and header row; JSON is newline-delimited JSON (NDJSON); timestamps are ISO 8601 with timezone offset; currency amounts have 2 decimal places with '.' as the decimal separator and include the currency code. - Given large datasets (>100k rows) are requested, When an export is initiated, Then a background job is queued and a download link is delivered via email and in-app within 15 minutes; the link expires 24 hours after generation. - Given schema versioning, When an export is generated, Then it includes metadata: export_id, generated_at, filters_applied, and schema_version in a header row (CSV) or metadata block (JSON). - Given a user cancels an in-progress export, When Cancel is requested, Then the job stops, is marked Canceled, and no partial file is available for download. - Given PII exists in audit records, When a user without PII.Export permission exports data, Then sensitive fields are redacted or omitted per policy while row counts remain unchanged.
Anomaly Alerts Configuration and Delivery
- Given alert settings are available, When an admin configures anomaly rules, Then they can define thresholds for: overage rate day-over-day change (e.g., >X%), exception rate exceeding Y%, and integrity hash validation failures, and save channel preferences (email, in-app). - Given an anomaly condition is met, When detection runs (≤5-minute cadence), Then a deduplicated alert is sent via the selected channels within 5 minutes, includes a link that opens the dashboard pre-filtered to the anomaly period, and suppresses repeats for the same condition within a 1-hour window. - Given quiet hours are configured, When anomalies occur during quiet hours, Then email notifications are suppressed and in-app notifications are queued; a daily summary is sent at quiet hours end. - Given a user reviews an alert, When they acknowledge or snooze it, Then its state updates (Acknowledged/Snoozed with duration) and this action is recorded in the audit trail with user and timestamp.
Data Retention and Privacy Controls
- Given retention policies are configurable, When an admin sets retention for audit logs and exports (e.g., 12/24/36 months) with optional legal holds, Then the settings save with effective date, owner, and scope, and are enforced going forward. - Given records exceed their retention period and are not on legal hold, When the nightly purge job runs, Then eligible records are deleted or irreversibly redacted within 24 hours, and a deletion log captures record ids/counts and reason codes. - Given backups contain purged records, When purge completes, Then corresponding backups are purged or expire within 30 days, and this action is recorded for audit. - Given SOC 2 practices, When any user views the audit trail, exports data, or changes retention settings, Then an access log records who, what, when, where (IP), and optional why (user-supplied reason), and this log is retained per policy. - Given a workspace is disabled, When retention runs, Then all active export links are invalidated immediately and future exports are blocked with an explanatory error.
Reconciliation Links and Dispute Lifecycle
- Given an overage audit record is viewed, When the user clicks the invoice link, Then the associated invoice opens in a new tab and displays payment allocation status (Paid/Partially Paid/Unpaid) with amounts and dates. - Given multiple payments apply to the invoice, When the user opens each payment link from the audit record, Then each payment view shows allocation details to the overage line item. - Given a dispute exists for an overage, When its status changes to Won/Lost/Withdrawn, Then the audit trail records the outcome and timestamp, and the Dispute Outcomes analytics update within 15 minutes. - Given the user clicks a Top Client/Service card, When navigating via the link, Then the dashboard re-filters to that client/service and totals match the sum of underlying audit records. - Given clients are billed in multiple currencies, When totals are displayed, Then amounts are shown in the workspace currency with the FX source and conversion timestamp recorded in the audit log.

Package Rules

Create reusable credit templates with session counts or time banks, expiration windows, carryover limits, and service-specific consumption (e.g., workshop = 2 credits). Apply rules with one click at purchase and let SoloPilot enforce them across scheduling, notes, and invoicing. Standardizes offers and ends ad‑hoc exceptions that leak revenue.

Requirements

Package Rule Template Builder
"As a solo practitioner, I want to create reusable package templates with clear rules so that I can sell consistent offers without manual setup each time."
Description

Provide a template builder to define reusable package rules, including unit type (credit-based sessions or time-bank minutes/hours), total allocation, start/activation conditions, expiration windows (fixed date or relative to purchase/first use), carryover limits per period, and overage behavior (block booking or allow overage at a defined billable rate). Enable per-service consumption definitions, rounding rules, and applicability constraints (service categories, staff, location). Support versioning and cloning of templates, validation for conflicting settings, and a preview of how rules apply to a sample client. Integrate with SoloPilot’s product catalog and checkout to apply a selected template at purchase, store the rule set on the client’s profile, and make it available to scheduling, notes, and invoicing services. Include role-based permissions for creating, editing, publishing, and retiring templates.

Acceptance Criteria
Credit-Based Template: Per-Service Consumption and Rounding
- Given a user with Template Creator permission, When they open the Template Builder, Then they can select Unit Type = Credits and enter Total Allocation as a positive integer. - Given the template is in Draft, When the user defines per-service consumption (e.g., Coaching Session = 1 credit, Workshop = 2 credits) and selects a rounding rule (Round Up to whole credits or Allow fractional credits), Then the configuration saves without error. - Given a saved draft, When the user clicks Preview and selects a sample client and a Workshop service, Then the preview shows 2 credits deducted and the remaining balance reflects the rounding rule. - Given the draft is valid, When the user clicks Publish, Then the template is saved as Version 1 with status Published and becomes selectable in the product catalog.
Time-Bank Template: Activation and Expiration Windows
- Given Unit Type = Time and Total Allocation = 10 hours with booking rounding = 5-minute increments (round up), When the user saves the draft, Then the draft saves successfully. - Given Activation = On First Use and Expiration = 30 days after activation, When the preview simulates first use date 2025-10-01, Then the preview shows expiration 2025-10-31. - Given Activation = On Purchase and Expiration = Fixed Date 2025-12-31, When the preview simulates purchase date 2025-09-22, Then the preview shows expiration 2025-12-31. - Given a 62-minute booking and a 5-minute round-up rule, When consumption is calculated, Then 65 minutes are deducted from the balance.
Carryover Limits Per Period Enforcement
- Given a credit-based template with Period = Monthly and Carryover Limit = 2 credits, When a month ends with 5 unused credits, Then exactly 2 credits carry to the next month and 3 expire. - Given the next month begins with 10 new credits and 2 carried over, When scheduling attempts to consume more than 12 credits, Then the system blocks the booking or applies overage per the template setting. - Given the preview runs for two consecutive months with defined usage, When carryover is computed, Then the month-by-month balances match the configured limit.
Overage Behavior: Block vs Allow with Billable Rate
- Given Overage Behavior = Block Booking, When the package balance reaches zero and a covered service is scheduled, Then booking is prevented and the user sees “Package exhausted—purchase required,” and no invoice is created. - Given Overage Behavior = Allow Overage with Rate = $100 per credit, When the balance is zero and a service consuming 2 credits is scheduled, Then the booking is allowed and an invoice line item for 2 overage credits at $100 each is created. - Given a time-bank template with Overage Rate = $2 per minute and Rounding = 5-minute round up, When a 12-minute booking occurs at zero balance, Then 15 minutes are billed and the invoice shows $30.
Conflict Validation: Prevent Invalid Settings
- Given Unit Type = Credits, When the user attempts to set time-based rounding increments, Then a validation error is displayed and Save is disabled. - Given per-service consumption includes a service outside Applicability Constraints, When saving, Then a validation error lists the invalid service and save is blocked. - Given Overage Behavior = Allow Overage, When the overage rate is blank or zero, Then save is blocked with an inline error. - Given Expiration Window = Relative with a negative duration, When saving, Then save is blocked with an inline error. - Given Carryover Limit per period exceeds Total Allocation, When saving, Then save is blocked with an inline error.
Versioning, Cloning, Publishing, Retiring, and Role Permissions
- Given a template in Draft v1 created by a Creator, When a Publisher publishes it, Then v1 becomes Published and immutable; further edits require creating v2. - Given Published v1, When an Editor clones it, Then a new Draft with a new template ID is created with identical settings and version reset to 1. - Given Published v1 is assigned to clients, When v2 is later published, Then existing clients retain v1 while new purchases default to v2. - Given a Retire action by an Admin, When the template is retired, Then it cannot be selected for new purchases but remains active for existing clients. - Given a user without Publisher role, When they attempt to publish, Then the action is blocked with a permission error and no state change occurs.
Catalog and Checkout Integration, Client Storage, and Service Enforcement
- Given a catalog product, When a Publisher associates a Published template to it, Then the product displays the template name and version in its details. - Given checkout for that product, When the purchase completes, Then the client profile stores the template reference (template ID and version) and initial balances per rules. - Given a scheduled booking for a covered service, When the booking is created, Then the system deducts the correct credits/time and enforces applicability constraints (service/staff/location) per the template. - Given session notes completion triggers invoicing, When the invoice is generated, Then it reflects package deductions and any overage as separate line items at the configured rates. - Given a preview with a sample client and simulated schedule, When the preview runs, Then it displays balances, expirations, carryover, and overage charges consistent with the template rules.
Service Consumption Mapping
"As a business owner, I want to define how each service consumes package credits or time so that bookings and billing automatically deduct the correct amount."
Description

Allow configuration of how each service consumes package value, including per-service credit multipliers (e.g., workshop = 2 credits, 90-min session = 1.5 credits), time-bank draw in minutes, and rounding behavior (up, down, nearest step). Provide defaults and overrides at service, duration, and category levels with inheritance rules. Display consumption impact in the service editor and scheduler. Maintain versioned mappings so existing packages use the mapping they were sold with while new purchases use updated rules. Surface warnings if a service has no mapping and apply a safe fallback. Integrate with the Services catalog, scheduling UI, and the deduction engine.

Acceptance Criteria
Credit Multiplier Calculation by Service and Duration
Given service "Workshop" has a service-level credit multiplier of 2.0 and no duration override And a client holds an active credit-based package When the scheduler books a 60‑minute Workshop Then the deduction engine calculates 2.0 credits for the appointment and stores the expected deduction on the appointment record And the Service Editor preview for Workshop displays "Consumes 2.0 credits" for 60 minutes
Time-Bank Draw and Rounding Behavior
Given service "Coaching Call" consumes time-bank minutes And rounding behavior is set to Nearest Step with step size = 15 minutes and ties round up When a 50‑minute appointment is scheduled Then the system calculates 45 minutes to draw from the time bank and stores the expected deduction on the appointment record And when a 53‑minute appointment is scheduled Then the system calculates 60 minutes to draw from the time bank and stores the expected deduction on the appointment record
Inheritance and Overrides Precedence
Given a global default of 1.0 credit per 60 minutes And category "Coaching" override = 1.5 credits And service "Deep Dive" override = 2.0 credits And service‑duration override for "Deep Dive" at 90 minutes = 2.5 credits When calculating consumption for a 90‑minute Deep Dive Then 2.5 credits are used (duration‑level override takes precedence) And when calculating a 60‑minute Deep Dive Then 2.0 credits are used (service‑level override takes precedence over category) And when calculating a 60‑minute Coaching service without a service‑level override Then 1.5 credits are used (category‑level applies) And when no overrides exist at category or service Then 1.0 credit is used (global default)
UI Shows Consumption Impact in Editor and Scheduler
Given a mapping exists for service "Strategy Session" at 1.0 credit per 60 minutes with rounding Up When a user opens the Service Editor for "Strategy Session" Then the UI displays the computed consumption for each configured duration and the selected rounding behavior And when a scheduler selects "Strategy Session" for a client with an active package Then the scheduler preview shows the expected deduction, including the applied rounding note and the mapping version identifier
Versioned Mapping Pinning at Purchase and Use in Deduction
Given mapping version v1 is active at time of purchase And a client purchases a 10‑credit package And later an admin publishes mapping version v2 that changes "Strategy Session" to 1.5 credits When the client books a "Strategy Session" using the previously purchased package Then the deduction uses mapping version v1 to calculate the credits And when the client purchases a new package after v2 is active Then bookings against the new package use mapping version v2 And the audit log for each deduction includes the mapping version identifier
Warning and Safe Fallback for Unmapped Services
Given service "Custom Workshop" has no mapping at duration, service, or category levels When an admin opens the Service Editor for "Custom Workshop" Then a visible warning is displayed that no consumption mapping exists And when a scheduler attempts to book "Custom Workshop" for a client with an active package Then a warning is displayed before confirmation And the system applies the safe fallback by not auto‑deducting any credits/minutes, tagging the appointment as "Unmapped Service — Requires Manual Billing," and logging the event
Deduction Engine Lifecycle and Idempotency
Given an appointment has an expected deduction stored at booking using the applicable mapping version When the appointment is completed and converted to an invoice Then the deduction finalizes equal to the stored expected amount and is recorded against the package balance And if the appointment duration is edited before completion Then the expected deduction is recalculated using the same mapping version and configured rounding rules And if the appointment is canceled Then the expected deduction is removed with no package balance impact And repeated completion/invoice events do not create duplicate deductions (idempotent)
Scheduling Enforcement and Balance Display
"As a scheduler, I want booking to respect a client’s package balance and rules so that I prevent overbooking and avoid manual exceptions."
Description

Enforce package rules at booking and rescheduling by checking client eligibility, available balance, and rule constraints before confirming slots. Support configurable behaviors for insufficient balance (block, waitlist, or allow with overage billing), hold policies (deduct at booking or upon completion), and cancellation/reschedule returns per policy and window. Show real-time remaining credits/time in the scheduler for staff and optionally on client-facing booking flows. Handle recurring bookings, time zones, group sessions, and multi-service appointments by calculating total consumption before confirmation. Integrate with calendar sync, client profile, and notifications to reduce revenue leakage and prevent exceptions.

Acceptance Criteria
Booking Confirmation Enforces Package Eligibility and Balance
Given a client with at least one active package that includes the selected service, When a staff member selects a time slot and service and attempts to confirm the booking, Then the system validates package status (active, not expired), service eligibility, and remaining balance before confirmation. Given validation passes and remaining balance covers the service's configured consumption (credits or minutes), When the booking is confirmed, Then the corresponding amount is reserved or deducted per active hold policy and the appointment status is set to Confirmed. Given the package is expired, not yet active, or the service is not included, When the booking is attempted, Then the system prevents confirmation and displays a specific error code and message stating the exact reason. Given the client has multiple eligible packages, When the booking is confirmed, Then the system selects the package according to the configured priority (earliest expiry first by default) or prompts the user to choose, and records the selected package ID on the appointment. Given booking is confirmed, When the appointment is saved, Then a ledger entry is created referencing the appointment ID, package ID, quantity consumed, and pre/post balances.
Insufficient Balance Behavior Configuration (Block, Waitlist, Overage)
Given organization setting = Block on insufficient balance, When remaining credits/time are less than required for the appointment, Then the system blocks confirmation and presents purchase/upgrade actions without creating the appointment. Given organization setting = Waitlist on insufficient balance, When remaining balance is insufficient, Then the system creates a waitlist entry with the requested slot, not a confirmed appointment, and sends notifications to client and staff. Given organization setting = Allow with overage billing and a default overage rate is configured, When remaining balance is insufficient, Then the appointment is confirmed and an overage charge line item is created for the deficit at the configured rate and linked to an invoice. Given Allow with overage billing is enabled but no payment method is on file and prepayment required = true, When remaining balance is insufficient, Then the system requires payment capture before confirmation and aborts confirmation if payment fails. Given Allow with overage billing and tax rules apply, When the overage line item is created, Then taxes are calculated per service tax settings and the invoice reflects correct totals.
Hold Policies and Cancellation/Reschedule Returns
Given hold policy = Deduct at booking, When an appointment is confirmed, Then the package balance is reduced immediately and the ledger entry is marked Held until completion or cancellation. Given hold policy = Deduct on completion, When an appointment is confirmed, Then no deduction occurs; a pending hold is recorded against the package and visible in balance details. Given a cancellation occurs outside the non-refundable window, When the appointment is canceled, Then held credits/time are fully returned and the ledger records a reversal with reason "Canceled - refundable window". Given a cancellation or no-show occurs inside the non-refundable window, When the appointment is canceled or marked no-show, Then the held amount is consumed and is not returned, with ledger reason set accordingly. Given a reschedule within the allowed window to a service with different consumption, When the reschedule is saved, Then the hold transfers to the new appointment and any delta in consumption is adjusted against the balance per policy. Given a recurring series is partially canceled, When one occurrence is canceled, Then only that occurrence's hold/deduction is returned or consumed per policy and the rest of the series remains intact.
Real-time Balance Display for Staff and Clients
Given a staff user selects a client and service in the scheduler, When a slot is highlighted, Then the UI displays the client's applicable package balances in real-time, including remaining credits/time, earliest expiry date, and carryover limits. Given multiple applicable packages exist, When the booking dialog opens, Then the UI highlights the recommended package (earliest expiry first) and allows manual selection before confirmation. Given client-facing booking balance display = enabled, When the authenticated client views the booking flow, Then their remaining applicable balance and earliest expiry are shown before confirmation. Given client-facing booking balance display = disabled, When the client views the booking flow, Then no balance information is shown and internal balances remain visible only to staff. Given the selected slot would occur after package expiry or exceed carryover, When the slot is selected, Then the UI shows a warning explaining why the balance cannot be applied.
Recurring Series, Multi-service, and Time Zone Consumption
Given a recurring booking with N occurrences and a defined recurrence rule, When the user reviews the series summary, Then total projected consumption equals the sum of each occurrence's service consumption and is validated against balance and policy before confirmation. Given a multi-service appointment including services with distinct consumption rules, When the appointment is validated, Then required consumption equals the sum of per-service rules and is enforced prior to confirmation. Given the staff and client are in different time zones or a daylight saving change occurs within the series, When consumption is calculated, Then it is based solely on service duration and is invariant to time zone offsets, and all stored times are normalized to UTC. Given insufficient balance to cover the entire series and policy = Block, When the user attempts to confirm, Then the system prevents series creation and displays the deficit. Given insufficient balance to cover the entire series and policy = Allow with overage billing, When the series is confirmed, Then overage billing is scheduled per occurrence and linked to the series.
Group Sessions Balance Validation and Consumption
Given a group session with capacity and a consumption rule of X credits per attendee, When attendees are added, Then each attendee's eligibility and balance are validated and consumption is reserved or deducted per hold policy. Given an attendee lacks sufficient balance and policy = Block, When the attendee is added, Then the system prevents adding and shows a deficit message with purchase options. Given policy = Waitlist on insufficient balance, When the attendee lacks sufficient balance, Then the attendee is added to the waitlist and not counted toward capacity until balance is available. Given the provider cancels the group session, When cancellation is executed, Then all attendees receive returns or consumptions per cancellation policy and are notified. Given a group session uses a service with a special rule (e.g., workshop = 2 credits), When attendees are confirmed, Then the rule is applied per attendee and recorded in each attendee's ledger.
Calendar Sync, Client Ledger, and Notifications Integration
Given calendar sync is enabled for the provider, When an appointment is created, rescheduled, or canceled, Then the external calendar event is created/updated/canceled accordingly and includes package reference in the description. Given a booking consumes or returns package balance, When the ledger entry is recorded, Then it includes timestamp, actor, appointment ID, package ID, consumption quantity, pre-balance, and post-balance values. Given an overage billing event is triggered, When the invoice is generated, Then it contains the correct line items, taxes, links to the appointment, and reflects payment status updates back to the booking. Given a block or waitlist occurs due to insufficient balance, When the action is taken, Then staff and client receive notifications per configured channels with actionable links (purchase, manage waitlist, or view booking). Given a booking is deleted by staff, When deletion is confirmed, Then all related ledger entries and calendar events are reversed or updated consistently and an audit log entry is created.
Automatic Deduction and Invoicing
"As a practitioner, I want credits to deduct automatically and overages to invoice themselves so that I don’t spend time reconciling sessions and chasing payments."
Description

Automatically deduct credits or time upon session completion or specified trigger (e.g., note sign-off) and reconcile with the client’s package. If usage exceeds balance or overage is allowed, generate an invoice for the delta at the configured rate, mark prepaid portions, and reference the package in line items. Support partial attendance adjustments, backdating when session details change, refunds/voids, and reversal of deductions when sessions are canceled within policy. Ensure taxes, discounts, and payment allocations align with existing SoloPilot invoicing flows and payment gateways. Provide idempotent operations to avoid duplicate deductions and ensure consistent ledger entries.

Acceptance Criteria
Deduct Credits on Note Sign-Off
Given a client with an active package P containing 10 credits and a completed 'Coaching Session' mapped to 1 credit And the session S has no prior consumption recorded When the provider signs off the session notes Then 1 credit is deducted from package P and the remaining balance shows 9 within 5 seconds And session S displays a consumption record linked to package P and a ledger entry LE with type 'consumption' And LE, S, and P references share a common transaction id for traceability
Overage Invoice Generation and Prepaid Allocation
Given client has package P with 1 remaining credit And service 'Workshop' consumes 2 credits And overage is allowed at a configured rate of $150 per credit When session S is completed and the trigger event is fired Then 1 credit is deducted from package P and marked as prepaid on session S And an invoice I is created for 1 overage credit at $150 with tax and discounts applied per account settings And invoice line items reference session S and package P and clearly label prepaid vs overage portions And the invoice total equals the calculated delta plus tax minus applicable discounts And payment automation executes per SoloPilot invoicing flows and any captured payment is allocated to line items correctly for the payment gateway
Cancellation Within Policy Reverses Deduction
Given session S was previously consumed against package P and/or invoiced for overage And the cancellation policy allows reversal up to 24 hours before start When the client cancels S within policy Then any consumption against P is reversed and credits/time are returned to P And any unpaid invoices linked to S are voided; any paid invoices generate a refund or credit memo per configured settings And ledger entries create reversal records linked to the original entries And session S reflects status 'Canceled (within policy)' with zero outstanding balance
Partial Attendance Adjustment
Given a time bank package P with 120 minutes remaining And session S is scheduled for 90 minutes and marked attended for 60 minutes When the provider signs off notes with a recorded duration of 60 minutes Then 60 minutes are deducted from P with remaining balance 60 minutes And if an initial 90-minute deduction existed, the system credits back 30 minutes and adjusts any associated invoices via a credit memo And rounding respects the configured granularity (e.g., 1-minute increments) with no more than 1 unit variance due to rounding
Backdated Change Reconciliation
Given session S was processed on 2025-09-10 consuming 1 credit from package P and generating invoice I And on 2025-09-20 an admin edits S to 'Workshop' consuming 2 credits effective as of the original session date When the edit is saved Then the system recalculates consumption as of 2025-09-10, deducts an additional 1 credit if available, or invoices the delta if overage is allowed And invoice I is adjusted (amendment or new adjustment document) to reflect the delta with an audit trail linking original and adjustment documents And all ledger adjustments are timestamped, balanced, and reference the prior transaction id chain
Idempotent Processing on Retries
Given trigger event T for session S is received with idempotency key K When T is processed multiple times due to retries or concurrent events Then only one consumption deduction and at most one invoice are created And subsequent identical requests return the existing transaction references without side effects And ledger balances and package P balance remain consistent with a single successful processing
Service-Specific Consumption and Package Eligibility
Given package P allows 'Coaching Session' (1 credit) and 'Workshop' (2 credits) and excludes 'Assessment' When session S is a 'Workshop' and the trigger occurs Then 2 credits are deducted from P or, if shortfall exists and overage is allowed, an invoice for the delta is created When session S is an 'Assessment' Then no deduction from P occurs and S is billed per standard pricing with no prepaid allocation And all invoice line items reference S and include metadata indicating package eligibility decisions
Expiration, Carryover, and Notifications Engine
"As a client, I want clear alerts before my credits expire so that I can use what I purchased without unexpected loss."
Description

Track package activation and expiration windows, enforce carryover limits per period, and apply optional grace periods. Run scheduled jobs to expire balances, with configurable behaviors for leftover credits (forfeit, convert to discounted overage rate, or one-time carry). Provide proactive notifications to clients and staff at configurable thresholds (e.g., 3 credits left, 14 days to expire) via email and in-app alerts. Display status banners on client profiles and booking screens. Allow temporary freezes/pauses with automatic date recalculation. Integrate with SoloPilot’s notification system, client timeline, and reporting to prevent revenue loss and client surprises.

Acceptance Criteria
Scheduled Expiration With Configurable Leftover Behaviors
Given a package activated on 2025-01-01 with 10 credits, an expiration window of 90 days, a grace period of 7 days, and leftover behavior set to forfeit And the workspace timezone is America/New_York When the expiration job runs daily at 00:05 workspace local time on the day after the grace period ends Then remaining credits are set to 0 And an Expired event is written to the client timeline with before/after balances, package ID, and timestamp And the package is locked from further consumption across scheduling, notes, and invoicing Given the same package but leftover behavior is convert_to_discounted_overage with an overage rate of $90 per credit When the job runs at 00:05 on the day after the grace period ends Then remaining credits are set to 0 And an invoice is generated with one overage line item referencing the package ID, quantity equal to the remaining credits, rate $90, and due date per billing settings And email and in-app notifications are sent to client and staff within 5 minutes, including a payment link Given the same package but leftover behavior is one_time_carry with a carry cap of 2 credits When the job runs at 00:05 on the day after the grace period ends Then up to 2 credits are moved into the next active period; any excess is forfeited And a Carryover event and a Forfeit event (if applicable) are appended to the client timeline and reflected in reporting
Proactive Threshold Notifications for Low Balance and Imminent Expiry
Given notification thresholds configured: low_credits = 3, days_to_expire = 14, notify channels = email + in-app And message templates are active with variables {client_name, package_name, credits_remaining, expiry_date, manage_link} When a client’s remaining credits drop to exactly 3 after a session is logged Then an email and in-app alert are sent to client and staff within 5 minutes using the configured templates And a Notification event is recorded on the client timeline And the same low_credits alert will not be resent for that package within a 24-hour dedupe window Given a package that will expire in 14 days based on workspace timezone end-of-day When the daily threshold evaluation job runs at 08:00 workspace local time Then a one-time 14-days-to-expire alert is sent via email and in-app to client and staff And the alert includes the exact expiry date and credits remaining And the alert status is visible in the notification center with unread/read states
Periodic Carryover Limits and Grace Application
Given a monthly period with carryover_limit = 2 credits and carryover_grace_days = 7 And a client ends the period with 5 unused credits When the month-end processing runs at 00:05 on the first day of the new period Then 2 credits are carried into the new period and 3 credits are processed per the configured excess behavior (forfeit, overage, or convert) And the carried credits are consumed first on subsequent bookings Given a carryover exists and the package is frozen during the first 3 days of the new period When the package is unfrozen Then the carryover availability window is extended by the freeze duration (3 days) And updated dates are reflected on the package detail and in reporting
Client and Booking Screen Status Banners
Given banner rules: show_low_balance at <= 3 credits, show_imminent_expiry at <= 14 days, colors {warning, danger} When a client opens their profile page with 2 credits and 10 days left Then a warning banner appears showing 2 credits remaining and 10 days to expiration with a CTA to purchase more credits And the banner state is synchronized with the notification thresholds and is not dismissible until the condition no longer applies Given a staff member opens the booking screen to schedule a session for a client with 0 credits and overage disabled When the booking form loads Then a danger banner appears and the Submit button is disabled with an explanation that no credits remain and overage is not permitted
Temporary Freeze/Pause With Automatic Date Recalculation
Given an active package expiring on 2025-04-01 and a freeze applied from 2025-02-10 to 2025-02-25 When the freeze is confirmed Then the expiration date is extended by 15 calendar days to 2025-04-16 And scheduled expiration and threshold jobs ignore the package during the freeze window And notifications related to low credits and imminent expiry are suppressed during the freeze and re-evaluated on unfreeze And a Freeze event and subsequent Unfreeze event are recorded with old/new dates in the client timeline Given a booking attempt during the freeze When a staff user tries to schedule a session from 2025-02-12 Then the system blocks consumption from the frozen package and offers to select another payment method or schedule after the freeze
Service-Specific Consumption and Overage Enforcement
Given package rules: session = 1 credit, workshop = 2 credits, overage allowed at $90 per credit And a client has 1 credit remaining and attempts to book a workshop When the booking is confirmed Then 1 credit is consumed and an overage invoice for 1 credit at $90 is automatically generated and linked to the booking and package And the client and staff receive invoice notifications, and the booking is marked as payable Given overage is disabled for the package When a client with insufficient credits attempts to book a workshop Then the booking is blocked with a clear error and CTA to purchase a top-up or new package
Reporting and Audit Trail Integration
Given reporting requirements for packages When the daily ETL runs Then the reporting dataset includes per-package and per-client fields: credits_purchased, credits_consumed, credits_expired, credits_carried, credits_forfeited, overage_credits_billed, overage_amount_billed, freeze_days_applied, notifications_sent_count And totals reconcile such that starting_credits + purchased - consumed - expired - forfeited - carried_out + carried_in = ending_credits Given any lifecycle event (activate, consume, notify, carry, expire, convert, freeze/unfreeze) When the event occurs Then an immutable timeline entry is created with actor, timestamp, package ID, and properties relevant to the event And exports (CSV) of reporting match on-screen aggregates within a 0.1% tolerance
Usage Reporting and Audit Trail
"As an owner, I want transparent usage and an audit trail so that I can spot leakage, resolve disputes, and optimize my offers."
Description

Provide dashboards and exports for package sales, utilization, remaining balances, expirations, overages billed, and revenue impact. Offer drill-down views per client, service, and template, with time-series charts. Maintain a tamper-evident audit log of all deductions, adjustments, refunds, expirations, and overrides, including actor, timestamp, reason, and linkage to sessions and invoices. Support manual adjustment tools with role-based permissions and required reason codes. Enable CSV export and API access for BI tools. Integrate with SoloPilot’s reporting module and client timeline to improve accountability and support dispute resolution.

Acceptance Criteria
Dashboard KPIs and Time-Series by Date, Client, Service, Template Filters
Given a user with Reports.View permission selects a date range and optional client, service, and template filters When they open the Usage Reporting dashboard Then the KPIs display totals for packages sold, credits/time purchased, credits/time consumed, remaining balance, credits/time expired, overages billed amount, and revenue impact (recognized + deferred) for the filtered scope And a time-series chart renders at the selected granularity (daily/weekly/monthly) within the date range And KPI values match the Export/API results for the same filters (tolerance ≤ 0.5%) And the dashboard loads within 3 seconds at the 95th percentile for up to 100k package events And empty states display zero values with a “No data for selected filters” message
Drill-Down From KPI or Chart Point to Client/Service/Template Detail
Given a user views the Usage Reporting dashboard When they click a KPI or a point/bar in the time-series chart Then a drill-down table appears scoped to the selected metric and time bucket with columns: Client, Service, Template, Purchased, Consumed, Remaining, Expired, Overage ($) And the drill-down supports sorting, pagination, and the same filters as the parent view And clicking a row opens the corresponding Client or Template detail with filters pre-applied And the drill-down totals reconcile to the parent metric within 0.5%
Tamper‑Evident Audit Log for Package Events
Given any deduction, adjustment, refund, expiration, or override is executed by system, user, or API When the event is recorded Then an audit entry is appended containing: event_id (UUID), event_type, actor_id and role, timestamp (UTC ISO 8601), client_id, package_id, service_id, session_id (nullable), invoice_id (nullable), before_balance, change_amount, after_balance, reason_code (from catalog), reason_note (optional), origin (system/manual/api), and a cryptographic hash chaining to the prior entry And audit entries are immutable (no update/delete) and only compensating entries are allowed And a “Verify Audit Log” action returns status=Valid for an untampered range; any alteration yields status=Invalid and displays a banner in the UI And the audit log is filterable by date range, client, event_type, actor, and exportable as CSV
Manual Adjustment with Role-Based Permissions and Required Reason Codes
Given a user attempts to adjust a client’s package balance When the user lacks Packages.Adjust permission Then the adjustment controls are hidden and any API attempt returns 403 When a user with Packages.Adjust initiates an adjustment Then they must select a reason_code from the admin-managed catalog, enter a numeric delta, and may add an optional note And validation prevents resulting balance < 0 unless the user has Packages.AllowNegative override and confirms via modal And upon submit, balances recalculate immediately across scheduling/notes/invoicing, an audit entry is created with actor and reason, and a note is added to the client timeline And the change is visible in dashboards, drill-downs, and API within 1 minute
CSV Export and REST API for Usage and Audit Data
Given a user with Reports.Export permission applies filters to a report or audit view When they request Export CSV Then a RFC 4180-compliant UTF-8 CSV is generated asynchronously with stable column order, ISO 8601 UTC timestamps, and download link expiring after 24 hours And datasets >100k rows stream or chunk without loss and the exported row count matches the on-screen total And API endpoints /reports/packages and /audit/events provide the same data with filters (date_range, client_id, service_id, template_id, event_type), cursor pagination, and p95 latency ≤ 2s for up to 200k events And each record includes cross-reference IDs to sessions and invoices when applicable
Expiration, Carryover, and Notification Reporting
Given packages have expiration windows and carryover limits defined by their templates When a package reaches its expiration date Then remaining balance is reduced per rule (to zero or capped carryover), an ‘Expiration’ audit event is recorded with before/after, actor=system, and reason=expiration And the dashboard shows counts for ‘expires in next 14/30/60 days’ and ‘expired in range’, matching drill-down details And sessions occurring before expiration consume eligible balances; sessions after expiration trigger overage per rule and are reported accordingly And upcoming expirations are exportable and any client notifications sent are logged on the client timeline
Overages Billed Calculation and Linkage to Invoices
Given a service consumes multiple credits (e.g., workshop=2) and a package’s balance is exhausted When additional sessions are delivered and invoiced Then overage amounts are calculated per service pricing and recorded as ‘Overage Billed’ audit events linked to session_id and invoice_id And the dashboard’s overage total equals the sum of linked invoice line items within 0.5% And clicking an overage in drill-down opens the associated invoice And issuing a refund or reversal creates a compensating audit entry and updates reports within 1 minute

Session Meter

A floating in‑session meter that shows remaining credits in real time as you capture notes or run a timer. Get gentle warnings at thresholds, then choose to wrap, convert overage instantly, or draw from a secondary bucket. Keeps sessions on track and turns extra time into revenue without breaking flow.

Requirements

Real-time Credit Meter Engine
"As an independent practitioner, I want to see a live view of a client’s remaining credits while I'm in session so that I can pace the session and avoid unbilled overages."
Description

Calculates and displays remaining client credits in real time during a session by pulling entitlements from packages, retainers, and prepaid plans. Supports both time-based (minutes/hours) and session-count credits, converts units as needed, and decrements accurately as the in-session timer runs. Handles grace periods, time zone differences, and schedule adjustments, and reconciles when sessions are edited or split. Updates instantly across the provider’s active devices and locks final consumption once the session is marked complete or invoiced. Integrates with SoloPilot scheduling, notes, and invoicing so the meter state directly informs billing outcomes.

Acceptance Criteria
Real-time decrement for time-based credits
Given a client entitlement with 90 minutes remaining When the provider starts the in-session timer Then the meter displays 90:00 and decrements every second, and the entitlement balance reduces in real time accordingly Given the timer is paused When the provider pauses the timer Then the meter stops decrementing within 200 ms and the entitlement balance remains unchanged during the pause Given the timer resumes When the provider resumes the timer Then decrementing restarts and total consumed time equals the sum of active run intervals
Session-count credit reservation and consumption
Given a client with 3 session-count credits and no time-based entitlements When the provider starts a session Then one credit is reserved and the meter indicates "1 in use, 2 remaining" Given the session is marked complete When the provider completes the session Then the reserved credit is permanently consumed (balance becomes 2) and the consumption locks Given the session is cancelled before completion When the provider cancels the session Then the reservation is released and the balance returns to 3 per the cancellation policy
Mixed entitlements and automatic unit conversion
Given a client with 30 minutes in Package A (later expiration) and 0.5 hours in Retainer B (earlier expiration) When the session timer runs Then the engine converts 0.5 hours to 30 minutes and consumes from Retainer B first based on earliest expiration Given the first entitlement is exhausted mid-session When remaining reaches 0 on Retainer B Then consumption automatically continues from Package A without stopping the timer and the meter labels reflect the active source Given multiple eligible entitlements When the provider manually selects "Draw from secondary bucket" Then the meter switches to the selected entitlement and billing mapping updates accordingly
Threshold warnings and instant overage conversion
Given alert thresholds set to 80% and 100% consumption of available credits When remaining credits reach 20% (80% consumed) Then a non-blocking visual warning appears and is logged with timestamp Given remaining credits reach 0 during a running session When the meter hits 0 Then the provider is prompted to choose Wrap, Convert Overage, or Draw from Secondary, without stopping the timer Given the provider chooses Convert Overage When overage begins Then a provisional invoice line item is created using the plan's overage rate and rounding policy, and overage time tracks in real time
Cross-device real-time sync and conflict prevention
Given the provider has the same session open on web and mobile When the timer starts, pauses, resumes, or completes on one device Then the other device reflects the change within 1 second and shows identical remaining credits and source entitlement Given one device is offline When it reconnects Then it reconciles to the server-authoritative meter state and no duplicate decrements are applied Given concurrent start attempts on two devices When both attempt to start the timer Then only one timer is allowed to run and the second device receives a conflict notice
Grace periods, time zones, and reschedule adjustments
Given a configured grace period of 3 minutes for the client When a session exceeds available credits by up to 3 minutes and the provider selects Wrap Then no overage is billed and the meter indicates "within grace" Given the client plan timezone differs from the provider timezone When determining applicable entitlements for a session at a DST boundary Then the engine uses the client's plan timezone to resolve the date and period and applies the correct entitlement Given an upcoming session with a reserved session-count credit is rescheduled to a new time before it starts When the schedule is updated Then the reservation follows the new appointment without consuming any credits
Finalization, reconciliation, and locking
Given a session is marked complete or invoiced When completion or invoicing occurs Then the final consumed credits (time or count) are locked and cannot be altered except by authorized adjustment Given a completed session duration is edited When the provider increases or decreases the recorded time Then the system runs reconciliation: consume additional credits or create overage for increases; return credits and issue a credit memo for decreases; all with audit logs Given a session is split into two appointments after completion When the split is confirmed Then the consumed credits are partitioned according to the split durations and corresponding invoices are updated to reflect the new allocations without double-charging
Configurable Threshold Alerts
"As a coach, I want configurable warnings before credits run out so that I can wrap up or adjust the plan without surprising the client."
Description

Provides gentle, non-disruptive warnings as remaining credits reach configurable thresholds (e.g., 75%, 90%, 100%). Alerts are visual and optional audio/haptic, respect Do Not Disturb, and are accessible (WCAG AA color contrast, screen-reader labels, keyboard dismiss). Thresholds can be set globally, per service, or per client, and warnings can be snoozed or dismissed without losing context. All alerts are logged against the session for later review to improve planning and client communication.

Acceptance Criteria
Effective Threshold Overrides by Level
Given global, service, and client threshold sets exist When a session starts for a specific client and service Then the most specific set applies in this priority: client > service > global Given some thresholds are missing at the most specific level When computing the effective set Then missing values inherit from the next less specific level without gaps Given a user saves a threshold set When validating inputs Then values must be numeric 0–100 inclusive, unique within the set, and ordered from high to low; otherwise show an error and do not save Given thresholds are updated during an active session When saved Then newly effective thresholds apply immediately; thresholds already fired do not re-fire unless remaining credits cross back above and then below the boundary again
Accessible, Non‑Disruptive Visual Alert
Given a configured threshold is crossed When the alert appears Then it renders as a floating, non-modal element in the session meter and does not steal focus from current input And the alert’s text/icons meet WCAG 2.2 AA contrast (>=4.5:1 for normal text, >=3:1 for large text/icons) in light and dark modes And the alert exposes an accessible name/description and is announced via aria-live="polite" And the alert can be dismissed or snoozed using keyboard only (Tab/Shift+Tab, Enter/Space to activate, Esc to dismiss) within two keystrokes And dismissing the alert preserves cursor position and scroll context
Optional Audio/Haptic Alerts Respect Do Not Disturb
Given audio and/or haptic alerts are enabled When a threshold alert fires Then the selected channels trigger in addition to the visual alert Given the OS Do Not Disturb/Focus mode is active When a threshold alert fires Then audio and haptic outputs are suppressed while the visual alert still appears Given the device lacks haptic capability When a threshold alert fires Then only supported channels are used without errors Given the user mutes audio/haptics mid-session When subsequent alerts fire Then those channels remain muted for the remainder of the session
Snooze and Dismiss Without Losing Context
Given a threshold alert is visible When the user selects Snooze for N minutes (1/3/5) Then all channels for that threshold are suppressed for N minutes and the session meter remains fully interactive Given the snooze period ends and the threshold condition persists or has progressed When time elapses Then the alert reappears once Given the user selects Dismiss on a threshold alert When remaining credits continue to decline Then the same threshold does not alert again in the current session, while higher-severity thresholds may still alert Then all snooze and dismiss actions are timestamped and logged against the session
Real‑time Triggering and De‑duplication
Given the remaining credits counter updates in real time When it crosses a configured threshold boundary from above to at-or-below Then the corresponding alert fires within 200 ms Given the counter fluctuates around a threshold When it does not cross from above to at-or-below again Then the alert does not re-fire (no spamming) Given a session tracks time or unit credits When thresholds are applied Then triggering is based on normalized remaining percentage regardless of unit type Given multiple thresholds are crossed in the same update window When alerts are displayed Then they are queued and shown sequentially, starting with the most urgent (lowest remaining) without overlap
Final Threshold Options: Wrap, Overage, Secondary Bucket
Given the final threshold is reached (e.g., 0% remaining / 100% used) When the alert appears Then it offers actions: Wrap Session, Convert Overage, Draw From Secondary Bucket (if available) When the user selects Wrap Session Then the session timer stops and billing remains within allocated credits When the user selects Convert Overage Then additional time/units beyond allocation are recorded as overage and an overage line item is added to billing When the user selects Draw From Secondary Bucket (available) Then remaining time/units are deducted from the secondary bucket and the meter continues without interrupting note entry Then all selections are logged against the session with timestamps and actor
Alert Event Logging and Review
Given any threshold alert event occurs When logging Then an entry records session ID, client, service, threshold value, remaining %, delivery channels (visual/audio/haptic), DND state, user action (auto, snoozed, dismissed, acted), and timestamps Given the session is viewed post-completion When opening the session log Then all alert events are listed in chronological order with filters by threshold and action Given an export is requested When exporting session activity Then alert events are included in CSV and JSON exports
One-click Overage Conversion
"As a therapist, I want to convert extra session time into a billable item with one click so that I capture revenue without breaking my flow."
Description

Allows providers to instantly convert time beyond available credits into billable overage from within the meter. Applies rate cards, rounding rules, and tax codes automatically, generating a line item on the existing session invoice or a new invoice as configured. If autopay is enabled, charges can be captured immediately; otherwise, invoices are sent with payment links. Updates the meter state, prevents double-charging, and supports undo within a short grace window with idempotent safeguards.

Acceptance Criteria
One-click conversion applies pricing and taxes correctly
Given a session has exceeded included credits and client/service-specific rate card, rounding rule, and tax code are configured When the provider clicks Convert Overage in the Session Meter and confirms Then the system computes billable overage using the configured rounding rule (ceil/floor/nearest with the configured increment) and applies the correct rate and tax code And a single overage line item is created with quantity (minutes or units), unit price, tax, and total matching the calculation within $0.01 And the line item description references the session ID/date and minutes billed And the updated amount due is visible to the provider in the meter within 1 second
Autopay capture and fallback behavior
Given autopay is enabled and a valid default payment method is on file When the provider confirms Convert Overage Then payment is captured immediately, the invoice is marked Paid, and a receipt is sent to the client and provider Given autopay is enabled but capture fails (e.g., insufficient funds, processor error) When the provider confirms Convert Overage Then the invoice remains Open with the overage line, a payment link is sent to the client, a non-blocking failure notice is shown to the provider, and no duplicate capture attempts occur without user action Given autopay is disabled When the provider confirms Convert Overage Then the invoice is created/updated with the overage, the invoice remains Open, and a payment link is sent to the client within 60 seconds
Idempotency and duplicate prevention
Given the provider clicks Convert Overage multiple times within 30 seconds or a retry is triggered due to network issues using the same idempotency key When the backend processes the requests Then only one overage line item is created and (if applicable) only one payment capture occurs And subsequent requests return a deduplicated response with the original invoice/charge references And the audit log records a single successful conversion with associated deduplicated attempts
Undo within 5-minute grace window
Given a conversion finished less than 5 minutes ago When the provider clicks Undo in the Session Meter Then the system reverses the overage: removes the line item if invoice is draft/open or posts a credit/void if posted, voids the capture if unsettled or issues a full refund if settled, restores the meter to the pre-conversion state, and records the reversal in the audit log And no residual balance or duplicate charges remain on the client account Given more than 5 minutes have elapsed since conversion When the provider attempts to Undo Then the Undo action is disabled and the provider is guided to create a manual adjustment/credit note
Invoice selection and linking logic
Given an open or draft invoice exists for the session When Convert Overage is completed Then the overage line item is appended to that invoice with preserved tax grouping and totals recalculated Given the prior session invoice is paid, voided, or locked When Convert Overage is completed Then a new invoice is created for the overage that references the session ID and prior invoice number for traceability Then in all cases, the invoice/line item clearly references the session ID/date and the minutes billed as overage
Meter state update and double-charge protection
Given the provider selects Convert Overage (not Draw from Secondary Bucket) When conversion completes Then the Session Meter displays Overage Billed for the converted minutes and those minutes are locked from further billing or bucket draw And a subsequent Convert attempt without additional elapsed time is blocked with an Already billed message and no new line items or charges are created And if additional minutes accrue after conversion, only the incremental minutes are eligible for a new conversion
Secondary Credit Bucket Selection
"As a freelancer, I want to pull from a secondary credit bucket when the primary is out so that I honor client agreements without manual adjustments."
Description

Enables drawing from alternate credit sources (e.g., corporate pool, retainer, grant) when primary credits are low or depleted. Presents balances and rules in-line within the meter, supports partial coverage and remainder-to-overage flows, and logs the allocation decision. Administrators can define fallback order, eligibility rules, and permissions. The meter updates to reflect the selected bucket in real time and synchronizes deductions with invoicing and client balance records.

Acceptance Criteria
Auto-Fallback Selection When Primary Credits Deplete Mid-Session
Given a session is active and the primary bucket balance reaches 0, when the meter registers depletion, then the next eligible secondary bucket in the admin-defined fallback order is automatically selected as the current source. Given auto-fallback occurs, when the switch completes, then the meter highlights the new source, displays its remaining credits and rule summary inline, and records a "Source switched by auto-fallback" event. Given no eligible secondary bucket exists, when the primary balance reaches 0, then the meter prompts the user to choose "Convert to overage" and no auto-selection occurs. Given auto-fallback is disabled by policy, when the primary balance reaches 0, then the bucket chooser is shown and no automatic switch occurs.
Manual Secondary Bucket Choice From Meter Drawer
Given multiple eligible secondary buckets exist, when the user opens the meter drawer and selects Change source, then the list shows each bucket name, remaining credits, rule summary, and eligibility badge in the admin-defined fallback order. Given the chooser is open, when the user selects a bucket and taps Confirm, then the active source changes within 500 ms and a confirmation toast appears. Given the chooser is open, when the user cancels or clicks outside, then the active source remains unchanged. Given a bucket is ineligible, when Show ineligible is toggled, then the bucket appears disabled with a reason string and cannot be selected.
Partial Coverage With Overage Conversion
Given the selected secondary bucket has fewer remaining credits than the projected session remainder, when the bucket depletes mid-session, then the meter automatically converts the remainder to overage at the configured rate without interrupting the timer. Given partial coverage occurs, when the session ends, then the invoice draft contains two line items: credits consumed from the bucket and overage time, each with correct quantities, rates, and bucket references. Given admin rules allow multi-bucket chaining, when the user selects Draw next bucket at depletion, then the meter switches to the next eligible bucket instead of overage and logs both transitions.
Real-Time Meter Update and Balance Synchronization
Given a bucket selection or depletion event occurs, when the event is applied, then the meter UI updates the source label, color, and remaining balance within 300 ms. Given a selection or depletion is applied, when data is synced, then corresponding deductions are posted to the bucket balance and client ledger within 2 seconds with an idempotency key to prevent double posting. Given the device is offline when a selection occurs, when connectivity resumes, then the pending change is reconciled once and the meter and ledgers converge to the server-authoritative balances.
Eligibility and Permission Enforcement for Secondary Buckets
Given per-bucket eligibility rules and user permissions exist, when the chooser loads, then only buckets the current user is permitted to draw from for this session’s context are selectable. Given a bucket becomes ineligible due to rule violation during the session, when the user attempts to switch to it, then the selection is blocked with an inline reason and a link to view rules. Given an admin has defined a fallback order, when eligibility filters are applied, then the meter uses the next eligible bucket in that order, and ineligible buckets do not affect order resolution.
Allocation Decision Audit Log and Invoice Linking
Given any source change (manual or auto) or overage conversion occurs, when the event is committed, then an immutable audit record is created with timestamp, actor (user or system), session ID, from_bucket_id, to_bucket_id or overage, quantities deducted, and decision reason. Given an audit record exists, when the user opens Session History or the invoice draft, then the allocation decision is visible with a link to the audit entry. Given a duplicate selection request is received, when the same idempotency key is detected, then no additional audit record or deduction is created.
Timer and Notes Overlay
"As a consultant, I want a floating meter that stays visible while I type notes so that I can monitor credits without switching screens."
Description

Renders a floating, draggable meter that anchors within the session’s notes and timer view. Provides compact and expanded modes, keyboard shortcuts, and snap-to-corner positioning so it never obscures essential fields. Displays remaining time, credits, active bucket, and status (e.g., approaching threshold). Integrates with note templates and the built-in timer API to start/stop/pause and to tag timeline events without interrupting typing.

Acceptance Criteria
Drag-and-Snap Overlay Positioning
Given the session notes and timer view is open and the overlay is visible When the user drags the overlay by its drag handle Then the overlay follows the cursor and remains within the viewport bounds of the session view And when released within 16 px of a screen corner, the overlay snaps to that corner with an 8 px margin And the final position (corner and offset) is persisted and restored when the view is reloaded for the same user and workspace
Compact vs. Expanded Modes
Given the overlay is visible When the user toggles the mode via the overlay control or the defined keyboard shortcut Then the overlay switches between compact and expanded within 200 ms And expanded mode shows remaining time, remaining credits, active bucket name, and status label And compact mode shows remaining time and a status icon, with a tooltip revealing full details on hover/focus And the last used mode persists across sessions for the same user and workspace
Timer Control Integration and Shortcuts
Given the note editor is focused during an active session When the user invokes the defined shortcuts to Start, Pause/Resume, or Stop the timer Then the built-in timer API is called with the corresponding action and returns success And the overlay state and the main timer display reflect the change within 250 ms And no characters are inserted into the note; the text caret and editor focus remain unchanged And if the API call fails, a non-blocking error toast is shown and no duplicate actions are triggered And when the user clicks the overlay timer controls, the same outcomes occur
Non-Obstruction of Essential Fields
Given the overlay is positioned in any corner When the session view is interacted with (including scrolling and opening tooltips/menus) Then essential fields (client header, session date/time, note editor toolbar, note body, primary timer controls) remain fully visible and interactive And if the overlay would overlap an essential element or its tooltip, it auto-offsets to maintain at least 8 px separation And when the window is resized below the default layout breakpoint, the overlay re-evaluates and repositions to avoid covering essential fields
Real-Time Meter Data and Status
Given the built-in timer is running or paused with an active credit bucket When time elapses or credits are consumed Then the overlay updates remaining time and remaining credits within 1 second of the change And it displays the active bucket label and current status (Normal, Approaching Threshold, Exceeded) And when remaining time/credits are less than or equal to the configured threshold, the status changes to Approaching Threshold with an amber accent And when remaining falls below zero, the status changes to Exceeded with a red accent And displayed values match the authoritative calculation within ±1 second or the smallest credit unit supported
Note Template Integration
Given a note template is applied, changed, or removed while the overlay is visible When the template content loads or re-renders the editor Then the overlay remains anchored and visible above the note content without flicker And the overlay does not steal focus from the editor or interrupt typing And if newly inserted template placeholders would be obscured, the overlay auto-offsets or snaps to keep them unobstructed And applying a template does not degrade overlay interactivity (drag, toggle, controls remain responsive)
Timeline Event Tagging Without Typing Interruption
Given the note editor is focused and a session timer exists (running or paused) When the user triggers Add timeline event via the defined keyboard shortcut or inline command Then a timeline event is created with the current timer timestamp and inserted at the caret as a marker without moving focus And the event appears in the session timeline list within 300 ms And undo/redo reverts/applies the marker in the note without affecting the timer state And no unintended characters are inserted into the note
Offline Meter and Sync
"As a field coach, I want the meter to keep working offline so that I can manage time and billing even without internet."
Description

Maintains a reliable meter experience during flaky or offline connectivity by using a local timer, cached balances, and deterministic decrement rules. Shows an "estimated" badge when authoritative balance cannot be confirmed, queues actions (e.g., overage conversion, bucket selection), and resolves conflicts on reconnection with clear reconciliation messages. Ensures no duplicate billing and preserves a seamless in-session flow regardless of connection state.

Acceptance Criteria
Local Timer Continuity During Network Loss
Given an active session meter with a confirmed balance checkpoint within the last 5 seconds When network connectivity drops or the API does not respond for 3 consecutive seconds Then the meter continues decrementing locally at 1-second intervals without freezing And the displayed remaining time updates every second And after 15 minutes of offline operation, the drift versus device clock is <= 1 second And if the app/tab is reloaded, the meter resumes using the persisted offline state within 1 second
Estimated Badge and UX State
Given the app cannot confirm authoritative balance When the meter switches to local mode Then an 'Estimated' badge appears within 500 ms on the meter And the tooltip text explains 'Using cached balance; final amount will reconcile on reconnect' And the badge is removed within 1 second after authoritative balance is re-confirmed And threshold warnings (e.g., at 80% and 100% usage) still trigger in offline mode
Offline Overage Conversion Queue
Given remaining credits reach zero while offline When the user taps 'Convert Overage' Then an overage conversion action is queued with an idempotency key and timestamp And the UI marks the overage as 'Queued' and disables duplicate taps from adding more queued items And on reconnect, the action transmits within 3 seconds and creates exactly one invoice line item And if the server rejects the conversion, the UI surfaces the error within 2 seconds and allows retry or alternative selection
Secondary Bucket Selection While Offline
Given the primary bucket is exhausted and a configured secondary bucket exists When the user selects the secondary bucket while offline Then the selection is stored locally with an idempotency key and effective timestamp And the meter immediately reflects the added balance as estimated And on reconnect, the selection applies server-side within 3 seconds without duplicate deductions And if the secondary bucket is unavailable or has insufficient balance, a reconciliation message explains the resolution and the meter reverts to the authoritative balance
Reconnection Reconciliation Rules
Given a confirmed checkpoint at time T0 with balance B0 and periods of offline usage And server-side consumption S and local consumption L occurred after T0 When connectivity is restored Then the system computes final consumption C = max(S, L) and updates the ledger to reflect C without double counting And a reconciliation message appears within 2 seconds summarizing L, S, C, and the final remaining balance And the meter transitions from 'Estimated' to authoritative within 1 second after reconciliation
Duplicate Billing Prevention and Retry Semantics
Given one or more offline-queued actions (e.g., overage conversion, bucket selection) When the app retries transmissions due to intermittent network flaps Then each action uses a stable idempotency key so that at most one corresponding server-side record exists And forced retry up to 3 times produces exactly one invoice line item or bucket change And the offline queue persists across reloads and device restarts for at least 24 hours And successful processing removes the item from the queue within 1 second and updates the UI state
Audit Trail and Access Controls
"As an owner, I want auditable records and role-based controls around meter actions so that billing is compliant and accountable."
Description

Captures an immutable log of meter events (threshold hits, snoozes, conversions, bucket draws) tied to the session and user, with timestamps and before/after balances. Exposes export and filter capabilities for finance and compliance, redacting PHI to meet privacy obligations. Provides role-based access so only authorized users can convert overages or change buckets, with workspace-level toggles to enable/disable the feature by role or service type.

Acceptance Criteria
Immutable Audit Log for Meter Events
Given an active session with Session Meter enabled When a threshold is hit, a snooze is initiated, an overage is converted, or a secondary bucket draw occurs Then an audit event is persisted with fields: event_id (UUID), workspace_id, session_id, user_id, event_type, timestamp_utc (ISO-8601), before_balance, after_balance, sequence_number (monotonic per session) Given an audit event exists When any user attempts to edit or delete it via UI or API Then the request is rejected with HTTP 403 and the stored event remains unchanged Given session activity generates events When querying the audit log by session_id Then events are returned ordered by sequence_number and each event becomes queryable within 2 seconds of the action
PHI-Redacted Audit Export
Given a Finance or Admin role requests an export with filters (date range, session_id, user_id, event_type, service_type) When the export is generated Then only matching events are included and the row count equals the number of filtered events Given an audit export is generated When inspecting its contents Then no PHI fields (client_name, client_email, free_text_notes) are present and client_id is used for identification Given a user selects CSV or JSON as the export format When the export runs on a dataset up to 100,000 events Then the file is returned in the chosen format with UTF-8 encoding within 30 seconds Given each exported record When validating columns Then it includes: event_id, workspace_id, session_id, user_id, role, service_type, event_type, timestamp_utc, before_balance, after_balance, units, rate, currency, bucket_id, invoice_id
Role-Based Permissions for Conversions and Bucket Changes
Given workspace policy toggles by role and service type When conversion or bucket change is disabled for a role or service type Then users matching the disabled criteria receive HTTP 403 and the action is not performed Given a user is permitted by current policy When they convert an overage or change the billing bucket Then the action succeeds and an audit event records actor user_id and the prior/new bucket_id or conversion details Given an unauthorized attempt occurs When access is denied Then an access_denied audit event is recorded with reason and no balance changes Given policy settings are updated and saved When the changes are applied Then the new policy is enforced within 60 seconds and a policy_change audit event is recorded with editor user_id and a summary of changes
Threshold and Snooze Event Logging
Given thresholds at 80% and 100% remaining credits When those thresholds are reached during a session Then a threshold_hit event is logged once per threshold per session with remaining_credits and timestamp_utc Given a threshold notice is shown When the user taps Snooze for N minutes Then a snooze event is logged with snooze_duration=N and next_warning_at timestamp and no balance changes Given multiple warnings and snoozes occur When they trigger during a single session Then threshold_hit events are deduplicated per threshold and each snooze is logged with a unique event_id
Secondary Bucket Draw Audit Details
Given a session has a configured secondary bucket When a draw from the secondary bucket is performed Then a bucket_draw event records source_bucket_id, amount_drawn, primary_before, primary_after, secondary_before, secondary_after Given the secondary bucket has insufficient funds When a draw is attempted Then the request is rejected with HTTP 409 and a bucket_draw_denied event is logged with reason=insufficient_funds Given multiple draws occur in one session When summing amount_drawn across bucket_draw events Then the total equals the cumulative draw displayed in the session summary
Overage Conversion Traceability
Given an overage conversion is confirmed When it is recorded Then an overage_conversion event includes units_converted, rate_applied, currency, tax_rate (if applicable), invoice_id (or null), before_balance, after_balance Given an overage conversion is later linked to an invoice When the invoice is generated or updated Then the original event remains immutable and only invoice_id may be populated post-fact without altering rate or units Given a user with permission attempts to reverse a conversion When reversal is allowed by policy Then a compensating overage_conversion_reversal event is created with reversing user_id and updated balances while the original event remains unchanged

Top‑Up Paylinks

When credits run low, SoloPilot auto-sends branded paylinks via email/SMS/DM with prefilled top‑up options and auto-renew toggles. Clients pay in a tap; credits replenish immediately and future bookings stay confirmed. Removes friction at the moment of intent and keeps cashflow steady.

Requirements

Real-Time Credit Threshold & Top-Up Triggers
"As a solo practitioner, I want credits to be monitored with proactive top-up triggers so that clients don’t run out before sessions and my future bookings stay confirmed."
Description

Continuously monitor each client’s credit balance and forecast run-out based on scheduled sessions and historical consumption to trigger top-up offers before credits block bookings. Support configurable thresholds per workspace, service, and client segment, including prediction windows (e.g., ensure ≥2 upcoming sessions covered). Generate a single idempotent trigger per threshold window and suppress duplicates while logging trigger rationale. Respect booking policies by holding upcoming reservations within a configurable grace window while payment is pending, keeping future bookings confirmed upon successful top-up. Integrate with the billing ledger to reflect reserved versus available credits and expose trigger events to automation logs and audits.

Acceptance Criteria
Forecast-Based Trigger Before Credit Run-Out
Given a client has scheduled sessions, historical consumption data, and a prediction rule requiring coverage for at least 2 upcoming sessions And requiredCredits = sum(credits for the next 2 scheduled sessions) and coverageShortfall = max(0, requiredCredits - availableCredits) When the monitoring service detects a change to balance, schedule, or configuration Then if coverageShortfall > 0, a TopUpTrigger event is created within 5 seconds and before any booking is blocked And the event includes clientId, workspaceId, serviceId, thresholdType=forecast, predictionWindowStart, predictionWindowEnd, requiredCredits, availableCredits, reservedCredits, coverageShortfall, rationale, dedupeKey, createdAt And triggerAt <= startTime of the earliest uncovered session
Hierarchical Threshold Configuration and Precedence
Given thresholds exist at workspace, service, and client segment levels and optional client overrides When evaluating a client-service pair Then the most specific applicable rule is used with precedence: clientOverride > service+segment > service > segment > workspaceDefault And disabling a rule at any level falls back to the next applicable level And configuration changes take effect within 60 seconds of save across the workspace
Idempotent Trigger Window and Duplicate Suppression
Given a TopUpTrigger exists for clientId+serviceId+thresholdType within a threshold window [windowStart, windowEnd] When re-evaluations occur within the same window Then no additional trigger is created And suppression is logged with reason=duplicateWithinWindow and correlationId And a new trigger is created only after the signal clears (balance crosses above threshold) and subsequently falls below again, generating a new window and dedupeKey And persistence enforces uniqueness on dedupeKey to ensure only one trigger is stored under concurrency
Grace Hold on Upcoming Reservations During Payment
Given an active TopUpTrigger and the client initiates a top-up checkout When checkout starts Then holds are applied to all at-risk upcoming reservations for a configurable graceDuration (default 2 hours) And bookings remain confirmed and are not auto-cancelled due to insufficient credits while the hold is active And if payment succeeds before expiry, holds are released and bookings remain confirmed And if payment fails or graceDuration expires, holds are released and standard booking policies apply
Ledger Entries for Reserved vs Available Credits
Given reservations are held due to a TopUpTrigger When ledger entries are written Then reservedCredits equal the sum of credits required for the held reservations and availableCredits excludes reservedCredits And the invariant totalCredits = reservedCredits + availableCredits holds after each transaction And on successful top-up, availableCredits increase by the purchased amount and holds convert to confirmed coverage without duplication (idempotent by transactionId) And on trigger closure without payment, temporary reservations are released and reflected in the ledger within 5 seconds
Automation Logs and Immutable Audit Records
Given any trigger creation, suppression, window open/close, hold start/expire, or ledger adjustment occurs When the event is processed Then a structured AutomationLog entry and immutable Audit record are written with fields: eventType, clientId, workspaceId, serviceId, dedupeKey, correlationId, rationale, configSnapshotVersion, occurredAt And events are queryable via API/console by clientId/workspaceId/time range for at least the past 90 days And Audit records are append-only; corrections are recorded as new events with a relatesTo reference
Dynamic Re-evaluation on Schedule and Price Changes
Given a client’s upcoming sessions or service credit cost changes (create, cancel, reschedule, price change) When the change is saved Then the forecast and threshold evaluation re-run within 5 seconds And existing triggers, holds, and ledger reservations are adjusted: closed if coverageShortfall = 0, or updated if coverageShortfall increases And all adjustments are logged to Automation Logs and Audit with updated rationale
Branded Paylink Generation with Prefilled Top-Up Options
"As a client, I want a branded paylink with a prefilled top-up amount and an auto-renew toggle so that I can pay in a tap without figuring out how much to add."
Description

Create unique, signed, expiring paylinks that open a branded payment page matching the workspace’s logo, colors, and tone. Prefill recommended top-up amounts based on plan, usage forecast, and minimums, while allowing a custom amount entry within configured bounds. Present an auto-renew toggle with clear consent text and pricing, and surface supported fast-pay options (Apple Pay/Google Pay) when available for one-tap checkout. Optimize the page for mobile-first performance and accessibility (WCAG AA), minimizing fields and friction. Store link metadata (client, offer, expiry, recommendation logic) for analytics and support.

Acceptance Criteria
Signed, Expiring, Single-Use Paylink Security
Given a top-up paylink is generated, When opened before expiry without tampering, Then the payment page loads successfully. Given the paylink is opened after expiry, When the user navigates to it, Then an "Link expired" page is shown with a button to request a new link, And payment actions are disabled. Given any parameter affecting amount, client, or expiry is modified, When the link is opened, Then signature verification fails, And a 403 error is returned with no sensitive information. Given the paylink is successfully paid once, When it is opened again, Then the page shows "Already paid" and does not allow another charge. Given concurrent requests attempt to pay the same link, When processed, Then exactly one succeeds and others are declined as duplicates within 1 second.
Branded Payment Page Theming
Given a workspace with logo and color palette configured, When the paylink page loads, Then the page displays the workspace logo and applies primary/secondary colors exactly as configured, And no default SoloPilot branding is visible to the client. Given the branding is updated in the workspace, When a new paylink is generated, Then the updated branding appears on the payment page. Given a light or dark logo variant, When rendered on its background, Then contrast meets WCAG AA (text 4.5:1; large text/UI 3:1). Given the page is loaded, When inspecting styles, Then brand tokens (colors, logo URL) are sourced from the workspace configuration and not hardcoded.
Prefilled Recommendations and Custom Amount Bounds
Given the client's plan, usage forecast, and minimum credit threshold, When generating the paylink, Then the page displays three recommended top-up amounts calculated per business rules and labeled with expected coverage (e.g., "~4 sessions"). Given workspace min and max top-up bounds, When a custom amount is entered, Then values outside bounds are rejected with inline validation and the pay button remains disabled until valid. Given the workspace currency and locale, When amounts are displayed, Then they are formatted in the correct currency and locale without ambiguity. Given recommendations are calculated, When the page loads, Then the default selected amount equals the middle recommendation unless below the minimum bound, in which case the minimum permitted amount is selected.
Auto-Renew Toggle and Consent Capture
Given auto-renew is enabled in workspace settings, When the paylink page loads, Then the auto-renew toggle is visible and defaults to the workspace's configured state. Given the toggle is ON at checkout, When the user pays, Then consent text clearly states renewal amount, frequency, next charge estimate, cancellation method, and links to terms, And the user's opt-in, timestamp, IP, and user agent are stored with the transaction. Given the toggle is OFF, When the user pays, Then no recurring mandate is created and no recurring consent is stored. Given jurisdictional mandate text is required, When enabled, Then the pay button is disabled until the user acknowledges the mandate checkbox.
Fast-Pay Wallets and Fallback
Given the device/browser supports Apple Pay/Google Pay and the merchant is eligible, When the page loads, Then the corresponding wallet button is shown above card fields and is keyboard-focusable. Given a wallet flow succeeds, When the user authorizes payment, Then the charge completes without additional form fields and the receipt page is shown within 3 seconds p95 on 4G. Given a wallet flow is unavailable or fails, When attempted, Then the UI falls back to card entry without losing the selected amount or auto-renew choice. Given the device does not support wallets or the merchant is ineligible, When the page loads, Then wallet buttons are not displayed.
Mobile-First Performance and Accessibility
Given a Moto G4-class device on 4G, When loading the paylink, Then LCP <= 2.5s and TTI <= 3.5s at p95, And total JS <= 200KB compressed. Given keyboard-only or screen reader navigation, When interacting with the page, Then all interactive elements are reachable in logical order, have visible focus, and have accessible names/roles, meeting WCAG 2.2 AA. Given color contrast testing, When measured, Then all text and interactive elements meet or exceed AA contrast ratios. Given form validation errors occur, When they are triggered, Then errors are announced via ARIA live regions, associated with inputs, and no motion/animation blocks interaction.
Immediate Credit Replenishment and Metadata Logging
Given a successful payment, When the processor confirms the charge, Then the client's credit balance increases by the paid amount within 2 seconds and is reflected in the dashboard and API. Given future bookings that require credits exist, When the payment completes, Then tentative or held bookings remain confirmed without interruption. Given the paylink lifecycle, When generated and paid, Then metadata is stored: client ID, workspace ID, link ID, creation time, expiry, recommendations presented and selected, amount paid, currency, auto-renew selection, payment method, device, and signature verification result. Given a support lookup by link ID, When queried via admin API, Then all stored metadata is retrievable within 1 second and sensitive PAN data is never returned.
Multi-Channel Paylink Delivery & Tracking
"As a consultant, I want paylinks sent via the channel my client responds to with tracking so that payments happen quickly and I can follow up when needed."
Description

Deliver paylinks via email, SMS, and shareable DM-safe URLs using client-level channel preferences and consent records. Provide template management with personalization tokens, localized content, and quiet-hour/timezone rules, plus retries and fallback channels on non-delivery. Shorten and track links to capture delivery, open, click, and paid conversion events, writing them to the activity timeline. Support secure deep links that open the native wallet where possible and allow manual resend from the practitioner’s dashboard. Ensure compliance with opt-in/opt-out workflows, sender ID rules, and regional messaging regulations.

Acceptance Criteria
Preference- and Consent-Driven Channel Selection with Regional Compliance
Given a client profile has a channel preference order and current consent statuses per channel and country, And the system has valid sender configurations per region, When a top-up paylink is triggered due to low credits, Then the system selects the highest-priority channel with valid consent for the client’s country, And applies a sender ID that complies with regional rules for that channel and country, And does not send on any channel lacking opt-in or with withdrawn/expired consent, And if no eligible channels exist, no message is sent and a task is created for the practitioner with the reason, And the outbound record stores channel, sender ID, country, consent snapshot, and decision rationale.
Quiet-Hours and Timezone Scheduling
Given the client’s timezone is known and quiet hours are configured (e.g., 21:00–08:00 local), When a paylink notification would occur during quiet hours, Then the system schedules the send for the next allowed window start in the client’s local time, And no real-time send occurs during quiet hours for any channel, And the scheduled time is visible on the message record, And if the client pays before the scheduled time, the scheduled send is canceled and the record is updated accordingly.
Template Personalization and Localization Rendering
Given a selected message template contains tokens (e.g., {{client.first_name}}, {{credit_balance}}, {{top_up_options}}, {{paylink}}), And the client’s locale and currency are known, When the message is prepared for each channel, Then all tokens resolve to non-empty values and no unresolved tokens remain, And numbers, dates, and currency are formatted per the client’s locale, And the paylink is unique to the client and send attempt, And email applies brand theme assets; SMS content stays within 2 segments (<=306 GSM-7 chars) or is concatenated appropriately, And if any required token is missing, the send is blocked and an actionable error is surfaced to the practitioner.
Retry Logic and Fallback Channel on Non-Delivery
Given a paylink is sent via a primary channel, When the provider returns a hard failure or no delivery receipt within 15 minutes, Then the system retries the same channel once within 5 minutes, And if delivery is still unconfirmed, the system falls back to the next eligible, consented channel, And if delivery is confirmed on the primary channel, no fallback is attempted, And all attempts (channel, timestamp, provider status/error) are logged and visible on the activity timeline.
Shortened, Trackable Links and Conversion Event Logging
Given a paylink is generated for a client top-up, When the message is sent on any channel, Then the link is shortened with a unique token per send attempt, And delivery, open (where the channel supports opens), click, and paid conversion events are captured with channel and attempt ID, And each event appears on the client activity timeline within 60 seconds of occurrence, And the paid event records payment ID, amount, and currency, and marks the token as consumed.
Secure Deep Linking to Native Wallet
Given a client taps the paylink on iOS Safari with Apple Pay available, Then the Apple Pay sheet opens with the correct amount within 2 seconds; otherwise a secure web checkout is shown, Given a client taps the paylink on Android Chrome with Google Pay available, Then the Google Pay sheet opens; otherwise a secure web checkout is shown, And all links are HTTPS, DM-safe, and contain no embedded PII, And the link expires after the first successful payment or 24 hours, whichever occurs first; subsequent attempts return a safe expired state with an option to request a new link.
Manual Resend from Practitioner Dashboard
Given a practitioner views a prior paylink notification on the client timeline, When they click Resend and select a channel or Copy Link, Then the system validates consent, sender compliance, and quiet hours before sending, And enforces a minimum 60-second cooldown per channel for the same client to prevent spam, And a DM-safe shareable URL is copied when Copy Link is used, preserving tracking, And an audit entry is recorded with practitioner ID, timestamp, channel, template version, and outcome.
Instant Payment Processing & Credit Replenishment
"As a client, I want my credits to be available immediately after I pay so that my upcoming sessions stay confirmed without interruption."
Description

Integrate with payment processors (e.g., Stripe) to accept cards and wallets with SCA/3DS support, handling webhooks idempotently. On success, immediately credit the client’s account, update the billing ledger, and unpause any at-risk bookings so future sessions remain confirmed. Generate receipts, update invoices where applicable, and post real-time confirmations to the client timeline and practitioner notifications. Handle taxes, currency, and rounding per workspace settings, and gracefully manage failures with clear client messaging and retry options. Provide reconciliation reports and error dashboards for finance and support teams.

Acceptance Criteria
3DS‑Secured Card and Wallet Payment Credits Account Immediately
Given a client opens a top‑up paylink with a selected amount and a workspace requiring SCA And the client pays via a supported card or wallet (e.g., Apple Pay, Google Pay) that may require 3DS When the processor sends a payment_intent.succeeded event Then the client’s credit balance increases by the purchased top‑up amount in the transaction currency per workspace tax settings within 5 seconds of webhook receipt And a billing ledger entry is created with processor IDs (payment_intent, charge), currency, gross amount, tax, fees, and SCA/3DS outcome And the client sees a success confirmation with the new balance and a receipt reference without needing a page refresh And the transaction is marked Settled only after the ledger write succeeds; otherwise it remains Pending and is retried automatically
Idempotent Webhook Processing Prevents Double‑Crediting
Given the same successful payment event is delivered multiple times within 10 minutes When the system processes each delivery Then credits, ledger entries, receipts, invoices, timeline events, and notifications are created at most once And subsequent duplicate deliveries return HTTP 2xx to the processor and are recorded as Duplicate Ignored in audit logs And the idempotency key and processor event ID are stored and retrievable for at least 90 days
Auto‑Unpause and Reconfirm At‑Risk Bookings After Top‑Up
Given the client has one or more bookings in At Risk status due to insufficient credits And a top‑up completes successfully When the credit balance is updated Then bookings transition from At Risk to Confirmed in chronological order until the balance would go negative And all applicable transitions complete within 10 seconds of the balance update And client and practitioner receive confirmations referencing affected booking IDs And bookings that cannot be covered remain At Risk with a clear Insufficient Credits reason
Receipts, Invoice Updates, and Tax Application
Given a successful top‑up in a workspace with defined tax rules (inclusive or exclusive), currency, and rounding settings When the transaction is recorded Then a receipt PDF is generated with line items (credits purchased, tax), totals, currency, masked payment method, and processor reference IDs, and is emailed to the client and stored on the client record And if an open invoice exists that is eligible for application (e.g., negative balance or credit replenishment), the payment is applied and the invoice status updates accordingly; otherwise no invoice is modified And monetary amounts on receipt, ledger, and invoice match after applying currency minor units and workspace rounding, with discrepancy not exceeding one minor unit
Real‑Time Timeline Entry and Practitioner Notifications on Success
Given a successful credit top‑up When the ledger entry is persisted Then a timeline event is added to the client’s profile within 1 second containing amount, currency, tax, new balance, and processor reference IDs And the practitioner receives an in‑app notification and an email (if enabled) within 10 seconds summarizing the top‑up and any booking status changes
Graceful Failure Messaging and Client Retry Options
Given a payment attempt fails due to decline, 3DS timeout, or network error When the client views or refreshes the paylink Then a clear, localized error message is displayed including the processor decline code or error category without exposing sensitive data And the client is offered retry options: reattempt, choose a different payment method, or switch currency if enabled And no credits are applied, no bookings are unpaused, and the ledger records a failed transaction with error details And webhook retry backoff follows processor recommendations and caps retries while preventing duplicate user‑visible artifacts
Finance Reconciliation Reports and Error Dashboard
Given a finance or support user requests reconciliation for a date range and currency When the report is generated Then totals per currency for gross, fees, tax, refunds, net, and credits issued match the processor’s balance transactions for the same period within 0.5% or $0.01 equivalent tolerance And the report is downloadable as CSV and JSON and includes links or IDs for ledger entries and processor transactions And the error dashboard lists unprocessed or failed webhooks and ledger writes with timestamps, error categories, and a manual retry action that records actor and outcome And resolved items are removed automatically upon success or can be marked Acknowledged with justification
Auto-Renew Enrollment & Lifecycle Management
"As a practitioner, I want clients to self-enroll in auto-renew from the paylink so that credits replenish automatically and I avoid revenue gaps."
Description

Enable clients to opt into auto-renew directly from the paylink page with clear consent capture, storing a processor tokenized payment method. Support threshold-based or scheduled renewals, configurable top-up amounts, and maximum monthly spend caps. Implement dunning and smart retries on failed renewals, with client and practitioner notifications and easy pause/cancel controls. Maintain an auditable history of consent, renewals, changes, and notifications for compliance and support. Expose settings and status in the client profile and allow bulk configuration via workspace defaults.

Acceptance Criteria
Enrollment & Consent Capture from Paylink
- Given a client opens a Top‑Up Paylink with auto‑renew available, When the client selects a payment method and checks an unchecked consent box linked to terms, Then the system tokenizes the payment method and records consent metadata (client id, timestamp UTC, IP, user agent, consent text version, channel, payment method fingerprint) and sets Auto‑Renew status=Active. - Given enrollment succeeds, When the page refreshes, Then a success banner is shown and an email/SMS receipt is sent within 60 seconds containing auto‑renew terms, amount, trigger type, and pause/cancel instructions. - Given enrollment occurs, Then no charge is made unless an immediate top‑up is required to meet a threshold; When immediate top‑up is required, Then the charge executes and credits update within 10 seconds of authorization. - Given the consent box is not checked, When the client attempts to enable auto‑renew, Then enrollment is blocked with validation messaging and no token is stored. - Given the processor declines tokenization/setup, When the client submits, Then an error is displayed and no Active auto‑renew is created.
Auto‑Renew Triggers (Threshold + Scheduled) Execute and Replenish
- Given Auto‑Renew is Active with Trigger=Threshold and threshold T and top‑up amount A, When credits drop to or below T, Then exactly one renewal attempt is initiated within 30 seconds and no additional attempts occur for the same event within a 15‑minute debounce window. - Given a successful threshold renewal, Then A credits are added immediately after authorization, a ledger entry is recorded (idempotency key, amount, method), and any pending bookings remain confirmed. - Given Auto‑Renew is Active with Trigger=Scheduled (e.g., monthly on day D at 09:00 in the client’s time zone), When the scheduled time occurs, Then a renewal attempt is initiated within ±15 minutes respecting time zone and DST; if D is invalid for the month, Then the run executes on the last day of the month. - Given a scheduled run starts, Then an idempotency key prevents duplicate charges; if the job retries due to timeout, Then only one charge is captured. - Given Auto‑Renew status is Paused or Canceled, When a trigger condition occurs, Then no renewal attempt is made.
Monthly Spend Cap Enforcement and Alerts
- Given a Monthly Spend Cap C is set, When a renewal would cause MTD spend to exceed C, Then the charge is not attempted, Auto‑Renew status becomes Cap Reached, and notifications are sent to client and practitioner within 5 minutes with links to adjust cap or top up manually. - Given the cap is reached, When the month rolls over in the workspace time zone, Then MTD spend resets to 0 and Auto‑Renew status returns to Active without manual intervention. - Given partial charge behavior is disabled by default, Then no partial top‑up is attempted and a prevented‑renewal event is logged with intended amount and cap remaining.
Dunning and Smart Retries on Failed Renewals
- Given a renewal attempt fails with a soft decline or network error, Then schedule retries at 1 hour, 24 hours, and 72 hours (max 3 attempts) using processor‑recommended retry codes and mark Auto‑Renew status=Past Due during this period. - Given a renewal attempt fails with a hard decline (e.g., do_not_honor, expired_card, invalid_account), Then stop further retries, notify client and practitioner immediately with a secure link to update payment method, and require re‑authentication when mandated by SCA. - Given dunning is active, Then credits are not replenished until a successful charge; existing confirmed bookings remain confirmed for a 24‑hour grace period, after which new bookings requiring credits are blocked until balance is sufficient or auto‑renew recovers. - Given a retry succeeds, Then credits are replenished, status clears to Active, and a success notification is sent; audit logs include attempt number, reason code, and outcome.
Pause, Resume, and Cancel Controls
- Given Auto‑Renew is Active, When a client or practitioner selects Pause and confirms, Then Auto‑Renew status=Paused immediately, next_run=null, and no further trigger attempts occur until Resume. - Given Auto‑Renew is Active, When Cancel is confirmed, Then Auto‑Renew status=Canceled, the stored payment method token is detached for auto‑renew purposes, and re‑enrollment requires fresh consent. - Given Resume from Paused, When the terms version has changed since prior consent, Then the user must re‑consent before reactivation; otherwise, prior settings are restored. - Given any state change (Pause, Resume, Cancel), Then client and practitioner are notified within 5 minutes and an audit record is stored with actor, timestamp, channel, and before/after values.
Audit Trail and Settings Visibility in Client Profile
- Given any auto‑renew action (enroll, update settings, trigger, success, failure, retry, pause, cancel, notification sent), Then an immutable audit record is persisted with fields: actor, action, entity ids, before/after values, timestamp (UTC), IP, user agent, channel, and correlation/idempotency keys. - Given a practitioner opens a client profile, Then they can view Auto‑Renew status, trigger type, top‑up amount, threshold/schedule, cap, last run, next run, MTD spend vs cap, and masked payment method details; sensitive data is never revealed beyond PCI‑compliant tokens. - Given the audit log is displayed, Then records are filterable by date range, action type, and outcome and exportable to CSV; exports include a checksum header to verify integrity. - Given compliance requirements, Then audit and consent records are retained for at least 7 years and are read‑only except by retention policy purge.
Workspace Defaults and Bulk Configuration
- Given a workspace admin sets auto‑renew defaults (trigger type, threshold/schedule, top‑up amount options, dunning policy, debounce, monthly cap), Then new clients created thereafter inherit these defaults. - Given a workspace admin initiates Bulk Apply to existing clients, When they confirm the preview summary showing counts to be updated and exceptions, Then the changes are applied atomically with per‑client audit entries and a completion report (success, skipped, failed) is generated. - Given Bulk Apply runs, Then clients with existing Active custom settings are not overwritten unless Allow override is checked; clients are notified of any changes with opt‑out instructions. - Given defaults are updated, Then the effective settings source (Default vs Custom) is shown in each client profile, and reverting to default is a one‑click action with confirmation.
Security, Compliance & Anti-Fraud for Paylinks
"As an account owner, I want secure, expiring paylinks with compliance controls so that payments are safe and client trust is maintained."
Description

Protect paylinks with signed tokens, short expirations, single-use options, and rate limiting; allow server-side revocation and regeneration. Separate PCI data via the processor, encrypt PII at rest/in transit, and enforce least-privilege access with audit logging. Add bot and abuse protections (e.g., hCaptcha) on suspicious traffic and flag anomalous top-up patterns for review. Ensure compliance with regional messaging and payments regulations, consent capture, and data retention policies (GDPR/CCPA-ready). Provide security event telemetry and alerts to ops and support.

Acceptance Criteria
Signed, Expiring, Single‑Use Paylink Access Control
- Given a paylink is generated with singleUse=true and expiry=15m, When a client opens it within 15 minutes, Then the payment page loads and the token is marked redeemed on successful payment. - Given the same token is accessed after redemption or after expiry, When it is requested, Then the server returns 410 Gone with error code LINK_EXPIRED_OR_USED and no payment fields render. - Given a token with any altered claim or invalid signature, When it is requested, Then the server returns 401 Unauthorized, logs a SECURITY_INVALID_TOKEN event with a hashed tokenId, and no page content is served. - Rule: Token contains only linkId, clientId, exp, singleUse, and nonce; no PII present; default expiry is 15 minutes (configurable 5–30 minutes). - Rule: Tokens are signed with asymmetric keys (e.g., RS256/EdDSA), include kid, and verify against current or previous key; signing keys are rotated at least every 90 days. - Rule: All paylink endpoints enforce HTTPS and HSTS (min 6 months); HTTP requests redirect to HTTPS without logging query tokens.
Rate Limiting and Bot Mitigation on Paylink Endpoint
- Rule: Enforce rate limits of 10 requests/min/IP and 3 token validations/min/IP; exceeding returns 429 with Retry-After header. - Rule: Trigger hCaptcha when >5 invalid token attempts in 10 minutes or >20 requests/min/IP; passing challenge lifts the challenge for 60 minutes; failing blocks for 15 minutes. - Rule: Known automated/headless signatures and disallowed user agents return 403; allowlisted PSP/webhook IP ranges bypass challenges and limits. - Given a request is rate-limited or challenged, When the response is sent, Then a RATE_LIMITED or CAPTCHA_CHALLENGE event is logged with IP hash, UA, and hashed tokenId.
Server‑Side Revocation and Regeneration of Paylinks
- Given an operator revokes a paylink via API or dashboard, When the action is confirmed, Then all existing tokens for that link become invalid globally within 60 seconds and return 410 with code LINK_REVOKED. - Given a revoked link is regenerated, When regeneration occurs, Then a new token with a fresh nonce and kid is issued and prior tokens cannot be used; clients receive updated email/SMS within 2 minutes. - Rule: Only roles with paylink:revoke or paylink:regenerate may perform these actions; every action is audit-logged with actor, timestamp, reason, and correlationId.
PCI Scope Separation and PII Encryption
- Rule: Cardholder data entry uses the payment processor's hosted fields/checkout; SoloPilot systems never receive, store, or log PAN/CVV (validated by network and application logs). - Rule: PII (name, email, phone) is encrypted at rest with AES-256-GCM; keys are managed in KMS with automatic rotation at least every 90 days and dual control for key changes. - Rule: Transport security enforces TLS 1.2+ with modern ciphers; HSTS enabled; CSP restricts frames to the PSP domain and disallows inline scripts on paylink pages. - Rule: Logs and analytics redact PII and tokens; test traces confirm no PII/token leakage during paylink flows. - Rule: Annual PCI SAQ A (or equivalent) attestation is completed and stored with evidences accessible to compliance roles.
Least‑Privilege Access and Immutable Audit Logging
- Rule: RBAC ensures only roles with paylink:read may view paylink metadata; support roles see masked PII by default; no role can view full card data. - Rule: Privileged access is just-in-time and time-bound (max 60 minutes) with approval; access auto-revokes at expiry. - Given any create/update/delete on paylinks or tokens, When the action occurs, Then an immutable audit log entry is written with actor, role, targetId, before/after (redacted), IP, UA, and timestamp. - Rule: Audit logs are write-once (WORM) and retained for at least 1 year; security roles can query and export logs.
Anomalous Top‑Up Detection and Review
- Rule: The system flags anomalies within a 24-hour window when any occur: amount > 3× 30-day average; ≥3 top-ups from different countries; ≥5 declines; BIN country ≠ client country and amount > $500. - Given a high-severity rule triggers, When the top-up is attempted, Then credit issuance is placed on hold pending review and the client is notified that processing may take up to 24 hours. - Given a medium-severity rule triggers, When the top-up completes, Then credits are issued but the account is marked Needs Review and a case is created for ops. - Rule: Each alert opens a review case with reason codes, evidence, and approve/deny actions; decisions are logged and feed model tuning; notifications reach ops/support within 2 minutes.
Regional Compliance: Consent, Messaging, and Data Retention
- Rule: SMS/DM paylink messages are sent only to contacts with recorded consent (timestamp, method, source, region); STOP/UNSUBSCRIBE updates consent within 5 minutes and suppresses further sends. - Rule: Message templates include required sender ID, business name, and opt-out instructions; regional quiet hours (e.g., 08:00–21:00 local) are enforced before sending. - Rule: GDPR/CCPA controls: provide export of paylink-related personal data within 30 days of request; delete within 30 days unless under legal hold; retention periods are configurable per region and enforced. - Rule: Records of processing (RoPA), DPAs, and consent history are stored and auditable; link domains comply with regional regulations (e.g., branded links where required).
Admin Configuration, Branding & Performance Analytics
"As a business owner, I want to configure paylink behavior and see conversion analytics so that I can optimize top-ups and stabilize cashflow."
Description

Provide an admin UI to configure thresholds, recommended amounts, channel priorities, quiet hours, and auto-renew defaults. Support branding controls for paylink pages and message templates, with preview and test-send modes. Offer dashboards for send volume, delivery rate, open/click-to-pay conversion, median time-to-pay, revenue from top-ups, and cohort breakdowns. Enable A/B testing of amounts and copy with statistical summaries, and export data via CSV/API for finance ops. Surface insights and alerts (e.g., low conversion, high failures) to guide optimization and ensure steady cashflow.

Acceptance Criteria
Configure credit threshold and recommended top-up amounts
Given I am an admin with permissions to edit Top‑Up Paylinks settings When I set the low-credit threshold to 20 credits and recommended amounts to [20, 50, 100] in USD and click Save Then the settings persist, an audit log entry is recorded, and subsequent low-credit events use the updated threshold And generated paylinks display the three recommended amounts in the configured order and currency And validation prevents non‑positive values, duplicates, and mixed currencies with inline error messages And changes propagate and are applied to new triggers within 60 seconds across all clients
Enforce channel priority and quiet hours for paylink sends
Given channel priority is configured as [SMS, Email, DM] and quiet hours are 21:00–08:00 in the account timezone When a low‑credit trigger occurs at 22:15 Then the paylink notification is queued and sent at 08:00 via SMS, respecting quiet hours When a send attempt via SMS returns a hard failure Then the system retries once on SMS within 5 minutes and then falls back to Email, then DM, maintaining a single active paylink per trigger And all attempts are logged with timestamp, channel, and provider response code, and delivery rate reflects provider “delivered” status And no duplicate charge links are created across fallback channels
Apply auto-renew default on paylink with consent capture
Given the tenant-level Auto‑Renew Default is On and the client has no override When the client opens the paylink Then the auto‑renew toggle is preselected On and displays clear recurring billing terms and a required consent checkbox When the client unchecks auto‑renew and completes payment Then the payment is processed as one‑time and auto‑renew remains disabled When the client completes payment with auto‑renew On Then the payment method token is stored, auto‑renew is enabled on the client profile, and a confirmation email is sent And all consent events (state, timestamp, IP, user agent) are recorded and the toggle state persists on page reload
Branding customization with live preview and test-send
Given an admin uploads a logo (PNG/SVG ≤ 1 MB), sets brand colors, and edits paylink page and message templates When the admin clicks Preview Then the paylink page and message previews render with the configured branding on desktop and mobile views When the admin performs a Test Send to a specified email/phone Then the message is delivered within 30 seconds, labeled as a test, and excluded from analytics and client history And invalid assets (unsupported type, size > 1 MB, bad hex color) are blocked with inline validation and guidance And previews render sample variables safely with masked sensitive data
Analytics KPIs with cohort filters and export parity
Given top‑up activity exists in the last 90 days When the admin filters by date range, channel, and cohorts (client segment, amount option, experiment variant) Then the dashboard shows send volume, delivery rate, open rate, click‑to‑pay conversion, median time‑to‑pay, and top‑up revenue for the selection And median time‑to‑pay is computed from send timestamp to payment timestamp; data freshness is ≤ 15 minutes And CSV downloads and the Analytics API return aggregates that match on the same filters with variance ≤ 0.5% And all metric tiles support drill‑down to event lists with pagination and export of the drilled data
A/B testing of amounts and copy with statistical summaries
Given an experiment is created with variants A and B, a 50/50 split, primary metric = click‑to‑pay conversion, and significance = 95% When the experiment is started Then assignment is randomized at the client level and remains consistent for repeat sends within 30 days And the dashboard shows per‑variant sample size, conversion, uplift, confidence intervals, and p‑value updated at least every 15 minutes When the experiment is stopped and a winner is selected Then the winning configuration is applied to defaults within 5 minutes and the experiment is archived read‑only And overlapping experiments on the same audience are prevented and minimum sample size warnings are shown
Insights and alerts for low conversion and delivery failures
Given alert thresholds are configured (e.g., conversion < 8% over 24h, delivery failures > 5% over 1h) When a threshold is breached Then an insight card appears with the impacted metric, scope, trend, and suggested actions (e.g., switch channel, update copy) And alerts are sent to configured recipients via email and webhook within 5 minutes and include a deep link to the filtered analytics view And alerts auto‑resolve when metrics recover for one full evaluation window and are suppressed during a configured cooldown to prevent duplicates And all alerts are logged with timestamp, condition, recipients, and status for audit

Split Ledger

Allocate credits to multiple buckets per client (e.g., Coaching vs. Workshops or by project) and define which services draw from which bucket first. Reports show burn by bucket and alert you when one stream nears zero. Ensures accurate billing for multi‑workstream clients and cleaner conversations with stakeholders.

Requirements

Bucket Management & Allocation
"As a solo practitioner managing multi-workstream clients, I want to create and fund separate credit buckets per client so that I can track and consume prepaid credits accurately by project or service."
Description

Enable creation and management of multiple credit buckets per client, each with a name, unit type (hours, sessions, or currency), initial balance, effective dates, optional expiration, and rollover policy. Provide UI and API to create/edit/archive buckets, fund or top-up balances, and view real-time balances and transactions. Enforce validation (e.g., non-negative balances unless explicitly allowed), prevent duplicate names per client, and support bulk import for initial setup. Persist an immutable ledger of bucket transactions (fund, consume, adjust, expire) to ensure accurate auditability. Surface bucket status in the client profile and session booking flows to guide selection and consumption. Ensure compatibility with existing billing rates and taxes without altering current invoice logic unless a bucket is applied.

Acceptance Criteria
Create & Edit Buckets via UI and API
Given a client When I create a bucket with valid fields (unique name per client, unit type ∈ {hours, sessions, currency}, initial balance ≥ 0 unless allow_negative=true, effective dates, optional expiration, rollover policy) Then the bucket is persisted and visible in UI and retrievable via API with matching values And audit fields (id, created_at, created_by) are recorded Given an existing bucket When I update editable fields (name, effective dates, rollover policy, allow_negative) Then changes are saved and reflected in UI/API And immutable identifiers remain unchanged Given a bucket is archived When I query active buckets Then the archived bucket is excluded from results and selection in booking And when I query with include=archived, it appears with status=archived
Field Validation & Duplicate Name Enforcement
Given a client When I attempt to create a bucket with a duplicate name (case-insensitive) under the same client Then the request is rejected with a clear validation error and no bucket is created Given a bucket payload When unit type is not one of hours/sessions/currency or rollover policy is not a supported value Then the request is rejected with field-specific errors Given initial balance < 0 and allow_negative ≠ true When creating or topping up a bucket Then the request is rejected with a non-negative balance validation error Given effective/expiration dates where expiration < effective start When creating or updating a bucket Then the request is rejected with a date-range validation error
Funding & Top-up Operations with Immutable Ledger
Given an active bucket When I fund/top-up with amount > 0 in the correct unit type via UI or API Then the bucket balance increases by the amount And a ledger entry of type fund is created with timestamp, amount, actor, and metadata Given existing ledger entries When I attempt to edit or delete a ledger entry Then the system disallows mutation and requires a compensating adjust entry by an authorized role Given a currency-based bucket When funding with decimals beyond the currency minor unit Then the recorded amount is rounded per currency rules and the rounded value is reflected in balance and ledger
Bucket Consumption in Booking and Invoicing with Service Mapping
Given a client has service-to-bucket mappings with priority When a billable session is completed or approved Then the system auto-selects the highest-priority eligible bucket with sufficient balance and decrements by the correct unit amount Given the selected bucket lacks sufficient balance When consumption is attempted Then the system applies the configured policy: partial consume to zero and bill remainder, or fallback to the next-priority eligible bucket, or block with a low-balance warning Given invoice generation When no bucket is applied Then existing invoice rates and tax calculations are unchanged And when a bucket is applied Then the invoice shows credit application without altering base rates/taxes, and totals reconcile with current logic minus applied credits Given a service not permitted by mapping When booking or attempting to apply a bucket via API Then that bucket is not offered for selection and the API rejects the application
Effective Dates, Expiration, Rollover, and Archiving
Given a bucket with a future effective start When booking or attempting consumption before the start date Then the bucket is ineligible for selection and cannot be consumed Given a bucket with an expiration date When the expiration date passes Then an expire ledger entry is created for the remaining balance (unless rollover applies) And the bucket status becomes expired Given rollover policy carry-forward with a cap When the rollover event occurs Then up to the cap amount is rolled into the next period with a rollover ledger entry And any excess is expired with an expire ledger entry Given a bucket is archived When attempting to fund or consume it Then the action is blocked and the bucket remains unchanged
Real-time Balances and Status Surfacing in Client Profile and Booking Flows
Given a client with multiple buckets When viewing the client profile Then each bucket displays current balance, unit type, status (active, future, expired, archived), effective/expiration dates, and last transaction time And balances reflect new transactions within 2 seconds of completion Given the booking flow for a service When choosing a payment/credit source Then only eligible buckets are shown with current balance and a near-zero indicator based on a configurable threshold And the default selection is the highest-priority eligible bucket Given concurrent consumption requests on the same bucket When they are processed Then balance consistency is maintained without going negative unless allow_negative=true And ledger entries are ordered and uniquely identifiable
Bulk Import for Initial Setup
Given a CSV file with required columns (client identifier, bucket name, unit type, initial balance, effective start, optional expiration, rollover policy, allow_negative) When I submit an import job with an idempotency key Then valid rows create buckets and initial fund ledger entries And invalid rows are rejected with row-level errors without blocking valid rows Given the same idempotency key is resubmitted with the same file When processing the import Then duplicate buckets are not created and the original results are returned Given a large import (e.g., 10,000 rows) When processing starts Then progress is reported and the job completes within the defined SLA And a downloadable error report is available upon completion
Service Mapping & Draw Order Rules
"As an account manager, I want to define which buckets fund which services and in what order so that consumption happens automatically without manual selection each time."
Description

Allow configuration of rules that map services/SKUs (e.g., Coaching Session, Workshop) to one or more eligible buckets with a defined priority order. When a service is consumed, the system attempts to draw from the highest-priority eligible bucket with available balance, falling back to subsequent buckets as needed. Support a client-level default bucket and global fallbacks when no explicit mapping exists. Provide rule-scoped conditions (e.g., by project tag, location, provider) and clear precedence. Validate to prevent circular or conflicting rules and surface a simulation view to test outcomes before saving. Allow opt-in behavior for spillover to standard billing if all buckets are depleted.

Acceptance Criteria
Configure service-to-bucket mapping with priority order
Given a service SKU "Coaching Session" and a client with buckets "Coaching" and "Workshops" When I configure a rule mapping the SKU to eligible buckets ["Coaching","Workshops"] with priority [1:"Coaching",2:"Workshops"] and save Then the rule is saved and retrievable with the same eligible buckets and priority order And the rule is active for the specified scope (client and conditions)
Draw from highest-priority eligible bucket with fallbacks and split across buckets
Given a mapping for SKU "Coaching Session" with priority ["Coaching","Workshops"] and balances Coaching=1, Workshops=5 When I record consumption of quantity 1 for "Coaching Session" Then Coaching decreases to 0 and Workshops remains 5 Given the same mapping with balances Coaching=0, Workshops=3 When I record consumption of quantity 1 for "Coaching Session" Then Workshops decreases to 2 Given the same mapping with balances Coaching=1, Workshops=2 When I record consumption of quantity 2 for "Coaching Session" Then Coaching decreases to 0 and Workshops decreases to 1
Fallback resolution: client default bucket then global fallback
Given no explicit mapping exists for SKU "Admin Fee" and the client default bucket is "General" with balance 4 When I record consumption of quantity 1 for "Admin Fee" Then "General" decreases to 3 Given no explicit mapping exists for SKU "Misc Service", the client has no default bucket, and a global fallback bucket "Standard" has balance 10 When I record consumption of quantity 2 for "Misc Service" Then "Standard" decreases to 8 Given neither client default nor global fallback is available and spillover is disabled When I attempt to record consumption for an unmapped SKU Then the action is blocked with an error stating no eligible bucket is available
Apply rule-scoped conditions (project tag, location, provider)
Given two rules for SKU "Coaching Session": Rule A applies when projectTag="Alpha" and provider="Dana" with eligible bucket ["Alpha Coaching"]; Rule B has no conditions with eligible bucket ["Coaching"] And the client has buckets "Alpha Coaching" and "Coaching" with sufficient balances When I record a "Coaching Session" for projectTag="Alpha" and provider="Dana" Then Rule A is selected and the draw comes from "Alpha Coaching" And when I record a "Coaching Session" for projectTag="Beta" and provider="Dana" Then Rule B is selected and the draw comes from "Coaching"
Precedence across overlapping rules: most specific match wins
Given three rules for SKU "Workshop": - R1 scoped to projectTag="Alpha" and location="NYC" with eligible bucket ["Alpha NYC Workshops"] - R2 scoped to projectTag="Alpha" with eligible bucket ["Alpha Workshops"] - R3 unscoped default with eligible bucket ["Workshops"] When I record a "Workshop" for projectTag="Alpha" at location="NYC" Then R1 is applied and the draw comes from "Alpha NYC Workshops" And when I record a "Workshop" for projectTag="Alpha" at location="LA" Then R2 is applied and the draw comes from "Alpha Workshops" And when I record a "Workshop" for projectTag="Beta" at location="NYC" Then R3 is applied and the draw comes from "Workshops"
Validation blocks conflicting or invalid rules
Given an existing rule for SKU "Coaching Session" scoped to projectTag="Alpha" When I attempt to create another rule for the same SKU with the identical scope Then the save is rejected with a conflict error that identifies the existing rule Given a rule whose eligible bucket priority list contains the same bucket more than once When I attempt to save the rule Then the save is rejected with a validation error indicating duplicate buckets in the priority list Given a set of rules whose evaluation would create a circular precedence among scopes When I attempt to save the rules Then the save is rejected with an error indicating a circular dependency and listing the involved rules
Simulation preview and opt-in spillover to standard billing
Given a new or edited rule set not yet saved When I run a simulation for SKU "Coaching Session" with specified project tag, location, provider, and current bucket balances Then the simulation shows the matching rule, the bucket(s) that would be drawn, the quantities deducted per bucket, and any remaining quantity And no actual balances or invoices are changed by the simulation Given all eligible buckets are depleted and the tenant preference "Spillover to standard billing" is enabled When I record consumption of quantity 2 for an eligible service Then 0 is deducted from buckets and an invoice line is created for quantity 2 at the SKU's standard rate with a note that buckets were exhausted Given spillover is disabled under the same conditions When I record consumption Then the action is blocked with an error and no invoice is created
Auto Drawdown & Session-to-Invoice Sync
"As a practitioner, I want credits to be automatically applied when I convert a session to an invoice so that I don’t have to manually reconcile what’s prepaid versus billable."
Description

Integrate bucket consumption into the one-click session-to-invoice flow: upon session completion or invoice creation, automatically deduct the corresponding quantity from the mapped bucket(s). Handle partial coverage: if a bucket lacks sufficient balance, consume the remainder from the next eligible bucket or convert the uncovered amount to billable line items. Ensure idempotency, preventing double deductions on retries, and support reversals on session cancellation or invoice void/refund. Display applied buckets and remaining balances on session and invoice views, and record detailed ledger entries linking consumption to specific sessions/invoices.

Acceptance Criteria
Auto Drawdown on Session Completion and Invoice Creation
Given a client with one or more credit buckets mapped to a service And a session for that service is marked Complete or an invoice is created with that service line item When the drawdown job runs synchronously with the action Then the system deducts the required quantity from eligible buckets until the line’s quantity is fully covered or buckets are exhausted And the consumption is recorded on the session/invoice with per-bucket amounts and pre/post balances And no covered quantity is billed as cash on the invoice; only uncovered quantity (if any) is billable
Bucket Priority and Eligibility Enforcement
Given a service is configured with a prioritized list of eligible buckets (e.g., A, then B) And the client has balances in those buckets When consumption occurs for that service via session completion or invoice creation Then credits are consumed in the configured priority order And ineligible buckets (not listed or disabled) are never used And if a higher-priority bucket has insufficient balance, the remainder is taken from the next eligible bucket
Partial Coverage and Uncovered Amount Billing
Given a session or invoice line requires Q units and eligible buckets can only cover C units where C < Q When drawdown runs Then exactly C units are deducted from the eligible buckets per priority And Q − C units become billable cash line item(s) on the invoice using the service’s standard pricing/tax rules And the invoice subtotal, tax, and total reflect only the uncovered quantity And no negative or over-consumption occurs in any bucket
Idempotency and Duplicate Event Handling
Given a session completion or invoice creation triggers drawdown with a unique correlation identifier per source (session/invoice) When the same event is retried, reprocessed, or the user repeats the action Then total bucket deductions for that source occur exactly once And exactly one set of ledger entries exists per bucket per source operation And balances remain correct with no double-deductions And the system returns a success response on duplicates without performing additional mutations
Reversal on Session Cancellation and Invoice Void/Refund
Given credits were consumed for a session or invoice When the session is cancelled before billing finalization or the invoice is voided Then 100% of the consumed credits are restored to their original buckets and reversal ledger entries are created linking back to the source And the session/invoice shows a clear reversal record Given an invoice is refunded partially When a refund for R units (or equivalent value) is applied Then up to R units of credits are restored in the original allocation order until the refunded quantity is matched And no bucket exceeds its pre-consumption balance And all reversals are idempotent (repeated refunds/voids do not double-restore)
UI Display of Applied Buckets and Remaining Balances
Given a session or invoice has undergone drawdown When a user opens the session view or invoice view Then an “Applied Buckets” section lists, per bucket: bucket name, amount consumed, pre-balance, post-balance, and timestamp And the current remaining balance per bucket is displayed for the client And if no credits were applied, the UI displays “No credits applied” with a link to allocation settings And updates appear within the same transaction so the UI reflects the final state without requiring a manual refresh
Ledger Entries, Atomicity, and Concurrency Safety
Given drawdown or reversal operations execute against one or more buckets When multiple sessions/invoices for the same client are processed concurrently Then operations are atomic and serialized per bucket so that balances never go negative and final balances equal initial − consumed + restored And each ledger entry includes: entry_id, client_id, bucket_id, source_type (session|invoice), source_id, operation (consume|reverse), quantity, unit, pre_balance, post_balance, created_at, actor_id/system, and correlation_id And ledger entries are immutable after creation and queryable for audit And if any step fails, no partial deductions remain (all-or-nothing rollback)
Burn Tracking, Threshold Alerts & Forecasting
"As a client success lead, I want proactive alerts when a bucket is running low so that I can secure extensions or top-ups before work is blocked or invoices surprise stakeholders."
Description

Provide real-time burn metrics per bucket, including consumed, remaining, and average burn rate over selectable windows. Enable configurable alert thresholds (e.g., 75%, 90%, 100%) and time-to-depletion forecasts based on recent consumption trends. Send notifications to internal users (email/in-app) and optionally client contacts, with deduplication and quiet-hour controls. Offer a depletion watchlist and dashboard badges on clients nearing thresholds. Allow per-bucket alert preferences and templates, and log alert history for compliance and follow-up.

Acceptance Criteria
Real-Time Burn Metrics Panel Per Bucket
Given a client with at least one active bucket When a user opens the bucket detail view Then the UI displays Consumed, Remaining, and Total credits accurate to the last 60 seconds And the figures include all posted transactions and exclude drafts or pending items And Average Burn Rate reflects the selected window (7, 14, or 30 days) as total credits consumed divided by days, rounded to one decimal with unit credits/day And when no consumption exists in the selected window, Average Burn Rate shows 0.0 credits/day with a tooltip "No consumption in window" And when switching between buckets for the same client, metrics update to the selected bucket within 500 ms And all timestamps display in the current user’s timezone
Selectable Window Burn Rate and Time-to-Depletion Forecast
Given a bucket with Remaining > 0 and Average Burn Rate > 0 When the user selects a burn window (7, 14, 30 days or Custom 3–90 days) Then Time-to-Depletion (TTD) is calculated as ceil(Remaining / Avg Burn) in days and an Estimated Depletion Date is displayed in the user’s timezone And when Average Burn Rate = 0, TTD displays "—" and Estimated Depletion Date is hidden with tooltip "No depletion forecast" And changing the window updates Avg Burn and TTD within 1 second And the default window is 30 days on first load
Configurable Per-Bucket Thresholds and Trigger Logic
Given per-bucket threshold percentages are configured (default 75%, 90%, 100%) When utilization percentage (Consumed / Total × 100) crosses a configured threshold upward for the first time since last reset Then a threshold-crossed event is recorded and notifications are enqueued for that threshold And thresholds can be added/edited (1–100%, max 10, no duplicates); invalid entries are prevented with inline validation And if utilization later drops below a triggered threshold and crosses upward again, a new event is recorded And at 100% utilization or Remaining <= 0, the 100% threshold event fires immediately
Quiet-Hour and Deduplicated Notifications to Internal and Client Contacts
Given an alert event (threshold crossed or forecast TTD <= configured days) When notifications are generated Then in-app notifications are sent to all internal users subscribed to that bucket's alerts and email notifications are sent to subscribed users if email channel is enabled And if client contact notifications are enabled for the bucket, emails are sent to the selected client contacts; otherwise they are excluded And deduplication ensures a recipient receives at most one notification per event per channel within a 60-minute window And alerts occurring during configured quiet hours are queued and delivered when quiet hours end; in-app badges may still appear immediately And messages use the selected template and include bucket name, client, Remaining, utilization %, Avg Burn window, TTD/date, and threshold reached And notifications are not sent to unsubscribed, bounced, or invalid addresses
Depletion Watchlist and Dashboard Badges
Given the depletion watchlist is enabled When a bucket crosses any configured threshold or TTD is less than or equal to the configured watch window Then the bucket appears on the Watchlist with Remaining, utilization %, Avg Burn window, and Estimated Depletion Date And client and bucket cards display badges: orange at >= 75% and < 100%, red at >= 100% or TTD <= 7 days And clicking a badge or watchlist row navigates to the bucket detail view And removing a bucket from the Watchlist manually is allowed but the bucket is re-added automatically if conditions persist
Per-Bucket Alert Preferences and Message Templates
Given a user with permissions edits bucket alert settings When they configure thresholds, forecast alert window (e.g., 7/14/30 days), channels (in-app/email), recipients (internal, client contacts), and quiet hours Then settings validate and save successfully, overriding workspace defaults for that bucket And the user can select and preview a message template with variables (client, bucket, Remaining, utilization %, Avg Burn, TTD/date) and send a test to self And quiet hours require a valid start and end; equal start/end disables quiet hours; overlapping inputs are rejected And a Reset to Defaults action restores workspace-level settings
Alert History Logging and Auditability
Given alerts are generated for a bucket When viewing Alert History Then each entry includes timestamp, client, bucket, event type, threshold/TTD values, metric snapshot, recipients, channels, dedup status, quiet-hour deferral (if any), and delivery outcomes And history entries are immutable, filterable by date range, client, bucket, and event type, and exportable to CSV up to 50,000 rows And alert history is retained for at least 24 months And each notification links to its corresponding history entry
Bucket Reporting & Exports
"As a business owner, I want clear reports of credit usage by bucket and service so that I can analyze profitability and plan renewals with data-backed insights."
Description

Deliver reporting that summarizes balances, burn over time, consumption by service/provider, and forecasted runout dates at client and portfolio levels. Include filters for date range, client, bucket, service, and project tags, with drill-through to underlying transactions. Provide CSV export and shareable links with configurable columns. Ensure report performance on large ledgers via pagination and server-side aggregation, and align data definitions with the ledger to avoid discrepancies with invoicing reports.

Acceptance Criteria
Client Bucket Balance & Burn with Filters and Drill-Through
- Given a user selects a date range, a client, one or more buckets, and optional service/provider filters, when they run the Client report, then the report shows Starting Balance (as of start date), total Credits, total Debits (Burn), and Ending Balance per bucket, and for each bucket and the grand total: Ending Balance = Starting Balance + Credits − Debits. - Given the same selections, when the user groups burn over time by week or month, then the sum of the time-series values equals the tabular Burn total for the range. - Given a user clicks a bucket row or chart segment, when drill-through opens, then the transaction list is filtered to the same client, bucket(s), date range, service/provider, and its count and sum match the rolled-up figures, and it loads within 2 seconds p95.
Portfolio Consumption by Service/Provider with Date & Tag Filters
- Given the user selects All Clients (or multiple clients), a date range, and one or more project tags, when the Portfolio report runs, then consumption is grouped by Service and Provider and only transactions containing the selected tags are included. - Given the same selection, when the user toggles Include Untagged, then rows without tags are included/excluded accordingly and totals update consistently. - Given the selection, when the user sorts by Total Burn descending, then the top 10 rows reflect correct ordering and pagination exposes remaining rows with consistent totals.
Forecasted Runout Dates per Bucket
- Given a bucket with positive Ending Balance and at least 10 days of non-zero burn in the last 30 days, when the report is generated, then Average Daily Burn = sum(debits in last 30 days) / (number of days with non-zero debits), and Runout Date = today + (Ending Balance / Average Daily Burn), rounded up to the next calendar day. - Given Average Daily Burn = 0 or Ending Balance = 0, when the report is generated, then Runout Date displays N/A. - Given high burn volatility (coefficient of variation > 1.0 over the last 30 days), when the report is generated, then a ± range is displayed using 25th and 75th percentile daily burn to compute earliest and latest runout dates.
CSV Export with Configurable Columns
- Given any report grid with selected visible columns and active filters, when the user exports to CSV, then only the visible columns in the current order are included, all filters are applied, timestamps are UTC ISO 8601, and currency values use 2-decimal precision. - Given the export would exceed 100,000 rows, when the user requests export, then a streamed/background export is generated and a download link is provided, and the file contains all rows with no truncation. - Given the exported CSV is opened in a spreadsheet, then column headers match UI labels exactly and the sum of exported amounts equals the on-screen totals for the same filters.
Shareable Links Preserve View and Enforce Access
- Given a user with access generates a shareable link, when the link is opened in an incognito browser, then the report renders read-only with the saved filters, grouping, and columns applied, shows latest data at open time, and actions (edit/delete) are disabled unless the opener authenticates with required permissions. - Given the owner sets an expiration date or revokes the link, when the expired/revoked link is accessed, then a 403 error page is shown and the report content does not render. - Given a shareable link is generated, when it is viewed by different users, then they all see the same configuration (filters/columns) and identical totals for the same timestamp of data refresh, subject only to data-access permissions.
Performance and Pagination on Large Ledgers
- Given a dataset of ≥5,000,000 transactions across ≥1,000 clients, when the Portfolio report loads with default filters, then initial aggregate metrics render within 3 seconds p95 and 5 seconds p99 using server-side aggregation (no client-side full data loads). - Given any tabular view with page size set to 100, when the user navigates to the next/previous page, then the page loads within 1 second p95 and the total row count reflects applied filters. - Given any combination of filters (date, client, bucket, service, tags), when applied, then requests complete without 5xx timeouts and no request exceeds 10 seconds p99.
Data Definitions Align with Ledger and Invoicing
- Given the same client, buckets, and date range, when comparing the Bucket report to the Ledger view, then totals for Starting Balance, Credits, Debits (Burn), and Ending Balance match within 0.01 currency units and use identical rounding rules. - Given invoicing reports for billable consumption in the same period, when services are configured to draw from buckets, then billed units/amounts reconcile to the report’s Burn by service/provider after applying an accrual cutoff at 23:59:59 UTC. - Given any metric definition is updated (e.g., what constitutes a Credit), when the change is released, then versioned definitions are captured and surfaced in UI tooltips and CSV metadata, and historical report results remain consistent with their original definitions.
Manual Adjustments & Audit Log
"As an administrator, I want controlled manual adjustments with a full audit trail so that I can correct errors without compromising financial integrity."
Description

Allow authorized users to post manual credit/debit adjustments to buckets with required reason codes, notes, and effective dates. Lock adjustments to finalized periods where applicable and require elevated permission for retroactive changes. Maintain an immutable audit log capturing who changed what, when, before/after values, and related entities (session, invoice, import). Provide reconciliation tools to compare ledger totals with invoice records and highlight discrepancies. Display change history on rule configurations and bucket settings for traceability.

Acceptance Criteria
Post Manual Credit/Debit Adjustment with Required Fields
Given I am an authorized user with Adjust Ledger permission And a client bucket exists and is active When I submit a manual adjustment specifying bucket, amount, type (credit or debit), reason code from the allowed list, notes, and an effective date Then the system validates all required fields and rejects the request if any are missing or invalid with a descriptive error And the adjustment is recorded with the specified effective date and correct sign applied to the bucket balance And the bucket balance and burn reports reflect the adjustment within the selected date range
Block Adjustments in Finalized Periods Without Override
Given the workspace has a finalized accounting period that includes the adjustment’s effective date And I do not have the Override Finalized Period permission When I attempt to post a manual adjustment for that period Then the system blocks the action, displays an error indicating the period is finalized, and makes no changes to any balances
Require Elevated Permission for Retroactive Adjustments
Given the effective date I enter is prior to today and not within a finalized period And I lack the Retroactive Adjustment permission When I attempt to post the adjustment Then the system blocks the action with an authorization error and no balance change occurs Given I have the Retroactive Adjustment permission When I post the same adjustment Then the system accepts it, applies it on the specified effective date, and records that it was retroactive in the audit log
Immutable Audit Log Captures Full Adjustment Details
Given a manual adjustment is posted to a client bucket When I open the audit log for that bucket Then I see an immutable entry that includes: actor (user id/email), timestamp, client, bucket, adjustment id, action (create/reverse), amount, type (credit/debit), effective date, reason code, notes, before/after balances, and any related entity (session/invoice/import) IDs And no one can edit or delete the entry; attempts are blocked and recorded as access-denied events And any correction must be performed via a reversing adjustment that links to the original entry And exporting the audit log includes these fields
Reconciliation Report Highlights Ledger–Invoice Discrepancies
Given I select a client and a date range When I run the reconciliation tool Then the system compares per-bucket ledger credits/debits to invoiced amounts for the same period and services And it lists each discrepancy where the absolute difference is greater than 0 with links to the underlying ledger entries and invoices And buckets with no discrepancy are marked as Reconciled And I can export the discrepancy list to CSV
Configuration Change History on Buckets and Allocation Rules
Given bucket settings or allocation rule configurations are created or modified When I view the Change History for a bucket or rule Then I see a chronological, immutable list of changes including who made the change, when, before/after values, and related entity references And I can filter the list by date range and user And no edits or deletions of history entries are permitted; attempts are blocked and logged

Risk Deposit Engine

Automatically sizes deposits by client history, time slot risk, and service type (fixed or percentage). High‑risk slots request a higher hold; reliable clients see lighter asks. Deposits auto‑convert to session credit when they attend or forfeit per policy on late cancel/no‑show—clearly messaged to reduce friction and boost show rates while protecting revenue.

Requirements

Risk Scoring Engine & Tiering
"As a provider, I want bookings automatically classified into risk tiers based on history and slot factors so that deposit sizing is fair, consistent, and protects my revenue."
Description

Implements a configurable scoring model that evaluates client reliability, time-slot risk, and service type to assign a real-time risk tier (e.g., Low/Medium/High). Inputs include client attendance history, cancellation/no-show rates, unpaid invoice flags, first-time status, slot characteristics (peak hours, short-notice), and service attributes (price, fixed vs. percentage eligibility). Supports configurable weights, default presets, and versioned model changes. The engine exposes a deterministic score and tier with an explanation payload used downstream for transparency, auditing, and support. Recalculates on booking, reschedule, and significant client history changes, with caching to protect performance. Integrates with Scheduling, Client Profile, and Invoicing modules via service interfaces.

Acceptance Criteria
Booking-time scoring returns deterministic score, tier, and explanation
Given a booking request with clientId, serviceId, slotId, and an active modelVersion When the scoring engine computes risk Then it returns a numeric score and a tier derived from the active model’s configured thresholds And repeated calls with identical inputs and modelVersion return the same score and tier And the response includes an explanation payload with factors [{factorKey, inputValue, weight, contribution, rationale}], modelVersion, calculationTimestamp, sourceEvent, and decisionId And the sum of factor contributions equals the score within ±0.1 tolerance And the response conforms to the published JSON schema and passes schema validation
Recalculation on reschedule/history change with cache TTL and invalidation
Given a cached decision for (clientId, serviceId, slotId, modelVersion) within the configured TTL When the engine is called with the same inputs during TTL Then it serves the response from cache and sets cacheHit=true When a reschedule event or a relevant client history change (e.g., new no-show, invoice marked unpaid) occurs Then the corresponding cache entry is invalidated within 5 seconds And the next computation returns cacheHit=false and reflects updated inputs And booking/reschedule invocations complete with P95 latency ≤ 300ms for computation and ≤ 800ms end-to-end under nominal load And event-driven recomputation after a relevant change occurs within 15 seconds of the change being committed
Configurable weights, presets, and versioned model management
Given an admin with permissions opens the risk model configuration When the admin selects a preset or adjusts factor weights and saves a new model Then validation enforces that each weight is within 0–100% and total weight sums to 100% before publish And a new modelVersion (semantic or monotonically increasing) is assigned and stored with metadata (author, timestamp, changelog) And the new modelVersion becomes active for new decisions within 60 seconds of publish without service downtime And each decision response and audit record includes the modelVersion used When the active version is rolled back to a prior version Then new decisions use the prior version within 60 seconds, and historical decisions remain linked to their original versions
Edge-case handling for first-time clients, missing data, unpaid flags, and service attributes
Given a first-time client with no attendance history When the engine computes risk Then it uses configured prior defaults for missing factors and marks each imputed factor in the explanation (imputed=true) When any required input is unavailable from a dependency Then the engine applies configured safe defaults, sets dataFreshness=stale for that source, and still returns a score and tier When the client has unpaidInvoiceFlag=true Then the explanation shows the unpaid_invoices factor with a positive contribution as determined by its weight When the service specifies price and fixed vs percentage eligibility Then these attributes are included as inputs and reflected in the explanation contributions And slot characteristics (peak hours, short-notice) are included per configuration and shown in the explanation
Service interface integration and idempotent API contract
Given the Scheduling module invokes the engine for booking or reschedule with correlationId and traceId When the request is processed Then the engine fetches inputs via Client Profile and Invoicing interfaces as needed and returns a 200 response with score, tier, explanation, modelVersion, decisionId, correlationId And repeated requests with the same correlationId within 5 minutes are idempotent, returning the same decisionId and not duplicating audit records When any dependency times out or returns 5xx Then the engine uses last-known values or defaults per policy, marks dataSourceStatus for each source, and returns a response without throwing an unhandled error And the request/response validate against the published service schemas in contract tests
Audit logging, transparency, and support traceability
Given any scoring decision is made When the decision is persisted Then an immutable audit record is stored containing clientId, serviceId, slotId, inputs snapshot, score, tier, modelVersion, eventType, correlationId, decisionId, actor (system/user), and timestamps And audit records are queryable by clientId, date range, and decisionId via API And the explanation payload for a decisionId is retrievable without recomputation with P95 ≤ 200ms And online audit data is retained for ≥ 365 days and archived for ≥ 7 years per policy And retrieving the same decisionId returns identical score, tier, and explanation to the original
Configurable Deposit Rules & Policy Editor
"As an admin, I want to configure deposit rules by risk tier and service so that the system enforces my policies without manual intervention."
Description

Provides an admin UI and rule engine to define deposit amounts by risk tier and service. Supports fixed amount or percentage-of-fee deposits, with min/max caps, currency-aware rounding, and per-service overrides. Includes policy elements for cancellation windows, late-cancel/no-show triggers, forfeiture logic, grace periods, first-time client requirements, VIP/exemption lists, and membership/prepaid pack exceptions. Features versioning with effective dates, sandbox preview with sample bookings, and a simulator for "what-if" outcomes. Changes are safely rolled out and logged, with policy snapshots attached to each appointment for accurate downstream enforcement and dispute handling. Integrates with Settings, Services, and Automations.

Acceptance Criteria
Percentage Deposits with Min/Max Caps and Currency-Aware Rounding
Given I am an admin in the Policy Editor with permission to manage Deposit Rules When I create a per-service rule for "Coaching - 60min": type=Percentage 30%, min=50, max=120, currency=USD and save Then the rule saves successfully, appears under Service Overrides, and becomes the active rule for that service When I run the simulator with service="Coaching - 60min" and fee=500 USD Then the calculated deposit equals 120.00 USD and is rounded to the currency's minor unit When I run the simulator with service="Coaching - 60min" and fee=100 USD Then the calculated deposit equals 50.00 USD (min cap applied) When I run the simulator with service="Coaching - 60min" and fee=300 USD Then the calculated deposit equals 90.00 USD (percentage within caps) When I switch currency to JPY and run the simulator with fee=20000 Then the calculated deposit equals 6000 JPY and is rounded to 0 decimal places per currency settings When I enter invalid values (percentage>100, min>max, negative amounts) Then inline validation errors are shown and Save is disabled
Fixed Amount Deposits by Risk Tier with Time-Slot and Client History Precedence
Given global fixed deposit tiers exist: Low=20 USD, Medium=40 USD, High=80 USD, and services default to "Use global tiers" And time slots Friday 16:00–20:00 are marked High-Risk And the risk tier selection rule is "use the highest applicable tier across client history and time-slot risk" When I simulate a booking for Service "Strategy Session" at Friday 17:00 with a High-Risk client Then the calculated deposit equals 80 USD (High tier) When I simulate the same booking with a Low-Risk client Then the calculated deposit equals 80 USD (time-slot High tier applies) When I simulate a Tuesday 11:00 booking with a Low-Risk client Then the calculated deposit equals 20 USD (Low tier) When I add a per-service override for "Strategy Session" High=100 USD and save Then subsequent simulations for that service use 100 USD for High tier regardless of global tier values When a service is set to "Override disabled" or the service is inactive Then global tiers apply and overrides cannot be selected for that service
Cancellation Windows, Grace Periods, and Forfeiture Outcomes Snapshot
Given policy settings: cancellation window=24 hours, grace period=10 minutes, late-cancel forfeiture=100% of deposit, no-show forfeiture=100%, attendance conversion=100% to session credit When an appointment is canceled 5 hours before start Then the deposit is marked forfeited and a policy snapshot with the applied rules is attached to the appointment When an appointment is canceled 30 hours before start Then the deposit hold is released (not captured) and the snapshot records "outside window — no forfeiture" When the client attends and the provider marks the session as attended Then the deposit auto-converts to session credit and the snapshot records the conversion When the appointment is marked no-show after the grace period Then the deposit is forfeited and the snapshot includes timestamp, actor, and rule trigger ("no-show")
First-Time Client Requirement, VIP Exemptions, and Membership/Prepaid Exceptions
Given policy settings: first-time clients require a deposit, VIP/exemption list bypasses deposits, members with active prepaid packs are exempt when balance>0 And precedence order is VIP > Membership/Prepaid > First-Time/General Rules When I simulate a first-time client (not VIP, no prepaid) for Service "Coaching - 60min" Then a deposit is required per the service/risk rule When I add the client to the VIP list and re-run the simulation Then the required deposit equals 0 and the UI indicates "VIP exemption applied" When I remove VIP status and add a prepaid pack with remaining credits>0 Then the required deposit equals 0 and the UI indicates "prepaid/membership exception applied" When prepaid credits=0 Then the deposit requirement falls back to the configured rule for the scenario
Versioning, Effective Dates, Rollout Controls, and Immutable Snapshots
Given Policy v1 is active in production When I create Policy v2, set an effective date of 2025-10-01 00:00 local workspace time, and publish Then new bookings created before the effective date use v1 and new bookings created on/after the effective date use v2 When I open any appointment created before the effective date Then its attached policy snapshot remains v1 regardless of future edits When I roll back to v1 or schedule v3 Then only bookings created after the rollback/effective date change use the reverted/new policy; existing snapshots remain unchanged When v2 is published Then an audit log entry is created with actor, timestamp, version diff, and an Automations event "PolicyUpdated" is emitted with version metadata
Sandbox Preview and What-If Simulator Has No Production Impact
Given I switch the Policy Editor to Sandbox mode When I edit rules and run the simulator against sample bookings (selected by service, client risk, time slot) Then results are displayed side-by-side for Current vs Draft with calculated deposits and rationale per case When in Sandbox mode Then no production policies are altered and no Automations events are emitted When I choose Publish from Sandbox Then I must confirm the effective date and changes before the draft becomes a new version in production When I discard the draft Then all sandbox changes are lost and production remains unchanged
Change Logging, Audit Trail, and Retrieval for Disputes
Given audit logging is enabled by default When any policy or rule change is saved, published, or rolled back Then an immutable audit record is created capturing actor, timestamp, action type, affected entities (rules, services), and before/after values When I view an appointment created under any policy Then I can view and export the attached policy snapshot showing all applicable rule values at booking time When I search the audit log by appointment ID or version ID Then I can trace the exact policy version and rule set that produced the deposit outcome
Real-time Deposit Calculation & Authorization
"As a client, I want the deposit to be calculated and authorized seamlessly during booking so that I can reserve my session with minimal friction and full clarity on the amount held."
Description

Calculates the required deposit during the booking flow using the current risk tier and active policy, then obtains payment authorization. Supports payment holds (preferred) and immediate charges when holds are unavailable, with gateway integrations (starting with Stripe) and tokenized payment methods. Handles multi-currency, authorization amount adjustments, idempotent retries, timeouts, and fallbacks. Enforces that a booking cannot be confirmed without a successful authorization (unless exempt). Persists authorization IDs and associates them to the appointment. Manages hold lifecycles (expiration, refresh, or conversion) and ensures PCI-aligned handling through the gateway. Exposes clear errors and recovery steps to the UI. Integrates with Booking, Payments, and Client Wallet components.

Acceptance Criteria
Real-time deposit calculation using risk tier and active policy
Given a booker selects a service, time slot, and client is identified And an active deposit policy is in effect with defined type (fixed or percentage), min/max caps, and tier rules And the current risk tier for the selection is resolved When the booking summary is computed Then the system calculates the deposit from the active policy and risk tier And applies min/max caps from the policy And rounds to the currency’s minor units per ISO 4217 And returns amount, currency, risk tier ID, and policy ID to the UI And displays the deposit before payment is requested
Authorization preference: hold first, fallback to immediate charge
Given the required deposit amount and selected payment method are known And the gateway supports authorization-only for the method and currency When the user confirms the booking Then the system requests an authorization hold for the deposit amount And persists the authorization intent/ID and status Given authorization-only is not supported or returns not_supported When the user confirms the booking Then the system attempts an immediate capture for the deposit amount after informing the user And records whether the transaction was a hold or capture and the fallback reason
Booking confirmation gated by successful authorization with exemptions
Given the policy requires a deposit and the client is not exempt When authorization or capture fails, is declined, or times out after retries Then the booking is not confirmed And the UI shows a clear error with retry and change-payment-method options Given the client or appointment meets an exemption rule (e.g., trusted client, zero-deposit policy) When the user confirms the booking Then the booking is confirmed without payment authorization And the exemption reason is stored for audit
Multi-currency calculation, display, and gateway submission
Given the service currency is supported by the merchant’s payment account When calculating and authorizing the deposit Then the system uses the service currency for both display and gateway requests And formats display per user locale while transmitting integer minor units to the gateway Given the currency is zero-decimal (e.g., JPY) When authorizing the deposit Then the amount uses zero decimals Given the service currency is not supported by the merchant account When attempting to proceed Then the system blocks confirmation and shows an actionable error to select a supported currency or contact support
Idempotent retries, timeouts, and duplicate-prevention
Given a gateway network timeout or 5xx occurs during authorization When retrying the request Then the system reuses a deterministic idempotency key tied to the booking attempt And retries up to 2 times with exponential backoff within 30 seconds And fetches the payment intent status after the final retry And ensures no duplicate authorizations/charges are created Given the final outcome remains ambiguous When presenting the result to the user Then the booking remains unconfirmed And the UI provides a resume link and guidance to retry or switch payment method
Persist authorization and manage hold lifecycle (refresh, convert, release)
Given an authorization hold succeeds When the appointment is created Then the system stores the authorization ID, amount, currency, hold/capture type, createdAt, and expiresAt And associates them to the appointment and client wallet Given the hold will expire before the session and policy requires a valid hold When approaching expiration Then the system attempts a refresh/incremental authorization for the required amount And updates the stored expiresAt upon success And notifies the user to re-authorize if refresh fails, marking the appointment as Action Required Given the client attends and the final invoice is generated When capturing funds Then the system captures up to the invoice amount and releases any remainder of the hold Given a late cancel/no-show per policy When capturing funds Then the system captures the policy-defined amount up to the hold value Given the appointment is canceled within the free-cancel window When processing the hold Then the system fully releases the hold
Tokenized payment methods, PCI alignment, and client wallet integration
Given the user selects an existing tokenized payment method from Client Wallet When authorizing the deposit Then the system uses the stored token and never transmits raw PAN/CVV through SoloPilot servers And records the token reference on the authorization intent Given the user adds a new card during booking When entering card details Then the system tokenizes via the gateway-hosted fields And stores only the tokenized method in the Client Wallet And marks the method available for future bookings Given tokenization fails or is declined by the gateway When informing the user Then the UI shows a clear error with retry and alternative payment options And no sensitive card data is persisted by SoloPilot
Auto-conversion and Forfeiture Workflow
"As a provider, I want deposits to auto-convert to credit when clients attend and be captured when they don’t, so that revenue is protected without manual follow-up."
Description

Automatically converts authorized deposits into session credit upon attendance and applies them to the invoice, or captures/forfeits them per policy on late cancellations and no-shows. Handles edge cases: partial deposits, multi-service sessions, over-collection guards (deposit never exceeds owed amount), tax alignment, and membership/prepaid scenarios. Releases unused holds promptly when appointments are canceled within policy. Ensures idempotent operations with reconciliation against invoices and payment gateway records. Generates client receipts and posts events to Automations (e.g., send confirmation, capture notices). Maintains a complete audit trail: policy version, timestamps, actor, gateway transaction IDs, and rationale.

Acceptance Criteria
Attend: Convert Deposit to Session Payment and Apply to Invoice
Given an appointment is marked Attended, an associated open invoice exists, and a valid deposit authorization exists for the client and appointment When the Auto-conversion workflow executes Then capture an amount equal to min(authorization_amount, invoice_remaining_balance) and apply it to the invoice as a payment And release any remaining authorization within 5 minutes And mark the invoice Paid if balance reaches 0; otherwise leave the correct remaining balance And generate and send a client receipt, storing a copy in the workspace And emit Automations event "deposit_converted" with appointment_id, invoice_id, policy_version, amount_captured, currency, gateway_transaction_id And append an audit record with policy_version, timestamps, actor=system, gateway_auth_id, gateway_capture_id, and rationale="attended"
Late Cancel/No-Show: Capture Forfeiture Per Policy and Reconcile
Given an appointment is marked Late Cancel or No-Show per the active policy and a valid deposit authorization exists When the Forfeiture workflow executes Then compute forfeiture_amount per policy (fixed or percentage) capped by min(authorization_amount, cancellation_fee_invoice_remaining_balance) And capture forfeiture_amount and apply it to the cancellation/no-show fee invoice And release any remaining authorization within 5 minutes And generate and send a receipt to the client And emit Automations event "deposit_forfeited" with appointment_id, invoice_id, policy_version, amount_captured, currency, gateway_transaction_id, rationale And write an audit entry with policy_version, timestamps, actor=system, gateway_auth_id, gateway_capture_id, and rationale="late_cancel/no_show"
In-Policy Cancellation: Prompt Deposit Release
Given an appointment is canceled within the policy window and a valid deposit authorization exists When the Release workflow executes Then void/release the authorization within 5 minutes and record deposit status=Released And ensure no invoice or payment is created And emit Automations event "deposit_released" with appointment_id, policy_version, amount_released, currency, gateway_transaction_id And send a release confirmation to the client And append an audit entry with policy_version, timestamps, actor=system, gateway_auth_id, rationale="in-policy cancellation"
Partial Deposit and Multi-Service Allocation with Tax Alignment
Given a multi-service appointment with line items that may have distinct tax rates and a deposit authorization that is less than the invoice total When conversion/capture occurs for attendance or forfeiture Then apply the captured amount to the invoice grand total (subtotal + tax - discounts), never exceeding the remaining balance And allocate the payment across line items proportionally to each line total including tax, using deterministic rounding (largest-remainder) with per-line rounding error ≤ $0.01 And store the allocation map in payment metadata and in the audit trail And verify that tax reporting at line level remains accurate post-allocation And release any unused authorization remainder within 5 minutes
Membership/Prepaid Coverage with Deposit Interaction
Given a client has membership/prepaid credits and a deposit authorization on the appointment When the appointment is Attended Then apply membership/prepaid credits first to the invoice; if any balance remains, capture from the deposit up to the remaining balance; if no balance remains, release the authorization within 5 minutes When the appointment is Late Cancel or No-Show Then capture deposit forfeiture per policy, capped by the cancellation-fee invoice remaining balance after applying any applicable membership benefits; if the policy exempts members, release the authorization And in all cases, ensure total collected (credits + deposit) ≤ invoice total and no over-collection occurs And record precedence, amounts, and calculations in the receipt and audit trail
Idempotent Operations and Payment Gateway Reconciliation
Given conversion/forfeiture/release operations may be retried by webhooks or jobs When the same operation is processed again with an identical idempotency key (appointment_id + operation_type + policy_version) Then no additional capture, release, or payment application occurs and the existing transaction is returned And the invoice balance remains correct and only one client receipt/event exists And a reconciliation job confirms a 1:1 mapping between internal payment_id and gateway transaction_id; any mismatch creates an exception record with status="Needs Reconciliation" and emits an alert event
Receipts and Automation Events Emission with Audit Trail
Given any deposit capture, conversion, or release completes successfully When notifications and events are generated Then send a client-facing receipt or release notice within 2 minutes via configured channels and store a durable copy And publish a single Automations event ("deposit_converted" | "deposit_forfeited" | "deposit_released") with schema: event_id, appointment_id, invoice_id (if any), policy_version, amount, currency, gateway_transaction_id, rationale, timestamps And provide at-least-once delivery with de-duplication by event_id; retry up to 3 times with exponential backoff on failures; log outcomes And persist a complete audit entry including policy_version, timestamps, actor, gateway IDs, and rationale
Client-facing Messaging & Consent
"As a client, I want clear deposit terms and consent prompts so that I understand what I’m agreeing to before confirming my booking."
Description

Delivers transparent, localized messaging across booking UI, email/SMS, and receipts explaining deposit amount, how it’s calculated, when it converts to credit, and forfeiture conditions. Includes a clear consent checkbox with a link to the full deposit policy, and dynamic summaries tailored to the client’s scenario (first-time, VIP, prepaid). Shows the deposit as a line item on confirmations and invoices, and surfaces conversion/capture events post-session. Ensures accessibility standards (WCAG) and mobile responsiveness. Provides content templates editable by admins and integrates with the Policy Editor for automatic updates.

Acceptance Criteria
Booking UI Deposit Disclosure & Consent Gate
Given a service requiring a deposit and a selected time slot, When the booking summary renders, Then the deposit amount is displayed as a currency line item with a short explainer of how it’s calculated (client history, time slot risk, service type). Given the booking form loads, When the consent checkbox is displayed, Then it is unchecked by default and the Confirm Booking button is disabled until checked. Given the policy link is clicked, When opened, Then it loads the latest published deposit policy matching the client’s locale in a new tab with the correct policy version ID in the URL. Given the client checks the consent box and confirms booking, When the booking is created, Then the system records consent metadata (client ID, timestamp, IP, policy version, locale) and associates it to the appointment and invoice. Given JavaScript is disabled, When the page loads, Then the deposit disclosure, policy link, and consent gate still render and function server-side.
Dynamic Messaging by Client Scenario (First-Time, VIP, Prepaid)
Given a first-time client with no booking history, When a high-risk slot is selected, Then the deposit message states that first-time bookings may require a higher deposit and shows the correct amount from the engine. Given a VIP/reliable client per policy, When any eligible slot is selected, Then the deposit message indicates a reduced deposit for reliable clients and shows the reduced amount. Given a client with an active prepaid package covering the service, When booking, Then the UI displays No deposit due—covered by prepaid credits and the consent checkbox is hidden. Given the client switches services or time slots, When risk factors change, Then the deposit amount and message update in under 300 ms without a full page reload. Given an admin-defined dynamic summary template, When variables are rendered, Then placeholders are fully resolved with no visible tokens.
Localization & Currency Formatting
Given supported locales (en-US, es-ES, fr-FR), When the client’s profile or browser locale is detected, Then all deposit-related texts use the appropriate translation and currency/number/date formats. Given a missing translation key, When rendering, Then the UI falls back to en-US strings and logs a missing-key warning visible in the admin localization report. Given the policy link is generated, When clicked in any supported locale, Then it opens the locale-specific policy page and anchors to the deposit section. Given SMS channel constraints, When sending a localized SMS, Then the message contains the deposit amount and forfeiture trigger within the first 160 characters and contains no unresolved placeholders. Given currency differs by locale, When displaying the deposit, Then the symbol placement, decimal/thousand separators, and rounding follow the locale conventions (e.g., $1,234.56 vs 1.234,56 €).
Confirmation & Receipt Messaging with Deposit Line Item
Given a booking requiring a deposit, When the confirmation email is sent, Then it includes a deposit line item with amount, calculation summary, policy link, and consent timestamp. Given SMS confirmation is sent, When delivered, Then it includes the deposit amount and a short line on conversion/forfeiture with a link to the policy or client portal. Given the invoice is generated at booking, When viewed, Then the deposit appears as a separate line item tagged Deposit (Hold) with amount and a reference to the appointment. Given the client pays or authorizes the deposit, When the receipt is generated, Then it shows authorization details (payment method last4, auth/transaction ID, date/time). Given the client cancels within the free window, When the booking is canceled, Then the deposit authorization is voided and the cancellation notice reflects Hold released in both email and portal.
Post-Session Deposit Conversion or Capture Surfacing
Given the client attends the session, When the session is marked Attended, Then the deposit auto-converts to session credit and is applied to the invoice with an Applied Deposit entry and a client notification is sent (email/SMS based on preferences). Given a late cancellation or no-show per policy, When the session status is updated accordingly, Then the deposit is captured and the invoice adds a Deposit Captured charge with reason and policy reference; a client notification is sent. Given the deposit is less than the session total, When converting to credit, Then the remaining balance due is displayed on the invoice and included in the notification with payment options. Given an admin overrides a conversion/capture outcome, When the override is saved, Then a timeline entry and updated receipt are generated and the client message reflects the override with editor, timestamp, and reason. Given the client portal Activity feed, When refreshed post-session, Then it shows a time-stamped deposit conversion/capture event with status and link to the invoice/receipt.
Admin Templates & Policy Editor Integration
Given an admin with permissions, When editing templates for Booking UI, Email, SMS, and Receipts, Then they can modify content using placeholders: {depositAmount}, {calcSummary}, {conversionCondition}, {forfeitureCondition}, {policyLink}, {policyVersion}, {clientType}, {serviceName}, {sessionDateTime}. Given the admin previews a template, When selecting a scenario (first-time/VIP/prepaid) and locale, Then the preview renders fully resolved content with correct formatting and links. Given the deposit policy is updated in the Policy Editor, When published, Then all channels automatically reference the new policy version and the consent flow displays the updated version ID within 60 seconds. Given a template is missing a required placeholder, When saving, Then validation fails and the error lists missing fields; save is blocked until resolved. Given template version history, When an admin reverts, Then the prior version is restored and a changelog entry records editor, timestamp, and diff.
Accessibility & Mobile Responsiveness Compliance
Given WCAG 2.1 AA standards, When testing the booking UI, Then text contrast ratios are >= 4.5:1, focus indicators are clearly visible, and the flow is fully operable with keyboard only (Tab/Shift+Tab/Enter/Space). Given a screen reader (NVDA/JAWS/VoiceOver), When navigating the deposit disclosure and consent controls, Then all labels, error messages, and policy links are announced with meaningful text and correct reading order. Given zoom set to 200% and reduced-motion preference enabled, When viewing the page, Then content reflows without horizontal scrolling and animations are reduced without hiding information. Given mobile breakpoints 320px, 375px, and 768px, When rendering, Then content is responsive, tap targets are >= 44x44 px, and no critical text is truncated or overlaps. Given form validation errors, When triggered, Then errors are programmatically associated to inputs (aria-describedby), announced to assistive tech, and persist until corrected.
Scheduling & Reschedule Reconciliation
"As a provider, I want deposits to update correctly when clients reschedule so that the coverage always matches the new risk without manual corrections."
Description

Keeps deposit alignment with risk when appointments are rescheduled or modified. On reschedule, recalculates the deposit for the new time slot and service, adjusts the authorization (increase, decrease, or reuse), and enforces policy windows and exceptions. Prevents rescheduling that would drop coverage below required thresholds without reauthorization. Handles provider-initiated moves, linked sessions, and back-to-back bookings. Synchronizes state across Calendar, Payments, and Invoicing, updating audit records and client notifications accordingly. Guards against duplicate holds and ensures a single authoritative deposit per appointment.

Acceptance Criteria
Client reschedules to higher-risk time slot requiring increased deposit
Given an appointment with an existing deposit authorization amount old_amount and a saved payment method And the Risk Deposit Engine calculates a new required deposit new_amount for the target slot and service where new_amount > old_amount When the client attempts to reschedule to that slot Then the system blocks the reschedule until the client completes re-authorization for the delta (new_amount - old_amount) And upon success, there is exactly one active authorization for the appointment equal to new_amount and the prior authorization is voided or closed And upon failure or cancellation of re-authorization, the reschedule is aborted and the original appointment time and authorization remain unchanged And the client is shown and sent a confirmation summarizing the new deposit amount and policy terms And an audit record captures before_amount, after_amount, actor=client, timestamp, policy_version, and payment_intent_id And no duplicate holds exist for the appointment at any time after completion of the flow
Client reschedules to lower-risk time slot and receives partial deposit release
Given an appointment with an existing deposit authorization amount old_amount And the Risk Deposit Engine calculates a new required deposit new_amount where new_amount < old_amount When the client reschedules to the new slot Then the system completes the reschedule without requiring client re-authorization when a valid saved payment method exists And the system replaces the prior authorization with a single authorization equal to new_amount and releases (old_amount - new_amount) per gateway rules And the release or void is confirmed by the gateway and stored with payment_intent_id and response_code And audit entries record before_amount, after_amount, release_amount, and actor And the client receives an updated confirmation showing the revised hold amount And no duplicate holds exist for the appointment after the flow
Reschedule attempt inside penalty window enforces policy and prevents evasion
Given the organization policy defines a reschedule cutoff and deposit forfeit rules And the current time is inside the penalty window for the appointment When the client attempts to reschedule to any future time Then the system applies the policy by either blocking the reschedule or requiring immediate policy payment or deposit forfeit, and displays the rationale And the deposit is converted according to policy (forfeit, partial forfeit, or credit) and any additional required coverage for the new slot is re-authorized before committing the reschedule And the invoice draft reflects any policy fee or forfeit line items, and calendar and payments states remain consistent And an audit entry records policy_applied=true, policy_rule_id, and resulting financial impact And exactly one authoritative deposit exists for the appointment after the operation with no duplicate holds
Provider-initiated move preserves coverage and single authorization
Given a provider initiates moving an appointment to a new slot And the Risk Deposit Engine computes new_amount from the new slot and service When new_amount <= old_amount Then the system reuses the existing authorization and completes the move with no client action, updating audit and notifications When new_amount > old_amount and the provider chooses to request coverage Then the client is prompted to re-authorize for the delta before the move is committed; on failure the move is reverted with no changes to the original appointment When new_amount > old_amount and the provider applies an allowed exception to waive the delta Then the move completes without client action, the authorization remains at old_amount, and the exception is logged with reason, actor=provider, and policy_exception_id And in all cases, exactly one active authorization exists for the appointment post-move and notifications indicate the initiator
Rescheduling linked/back-to-back sessions maintains correct combined coverage
Given two or more appointments are linked as a sequence or are back-to-back with independent deposits When the user reschedules only one of the linked appointments Then only that appointment’s required deposit is recalculated and reconciled, leaving the others unchanged, each with a single active authorization When the user reschedules the linked appointments as a group Then the system recalculates each appointment’s required deposit independently and runs a consolidated re-authorization flow that obtains approvals for the sum of all positive deltas while ensuring exactly one authorization per appointment And if any re-authorization in the group fails, none of the appointments are moved and all prior authorizations remain intact And audit entries reference a common correlation_id across the group with per-appointment before and after amounts
System synchronization, audit, and notifications on reschedule adjustments
Given a reschedule that changes deposit requirements When the reschedule completes successfully Then Calendar reflects the new time, Payments reflects a single active authorization with amount and status, and Invoicing reflects updated deposit or credit state And the client receives updated confirmation via configured channels and the provider sees updated details in the appointment view And an immutable audit trail includes initiator, timestamps, before and after amounts, policy rules applied, authorization IDs, and notification IDs And if any subsystem update fails, the entire reschedule is rolled back, no partial state persists, and the user is shown an actionable error
Duplicate hold protection and idempotency under concurrent reschedules
Given two or more reschedule attempts for the same appointment occur concurrently or via rapid retries When the system processes the requests Then optimistic concurrency or version checks ensure only one reschedule succeeds and others return a conflict with no side effects And payment operations are idempotent using idempotency keys so at most one authorization is created or updated And after all attempts settle, there is exactly one active authorization for the appointment and the appointment state is consistent across Calendar, Payments, and Invoicing And audit entries record each attempt with outcome, actor, and referenced payment intents
Reporting & Risk Insights
"As an admin, I want analytics on deposit performance and risk trends so that I can tune policies and demonstrate ROI."
Description

Provides dashboards and exports showing show-rate changes, deposit conversion vs. forfeiture, revenue protected, average deposit by tier/service/slot, client risk trends, and exception rates (declined authorizations, expirations). Supports time filters, cohort comparisons (e.g., policy versions), and drill-down to appointment-level audit records. Enables evidence for policy tuning and business decisions, with CSV export and webhook events for external BI. Aligns with SoloPilot’s reporting framework and respects role-based access and data privacy constraints.

Acceptance Criteria
Time-Filtered KPI Dashboard (Show-Rate, Conversion vs. Forfeiture, Revenue Protected)
- Given a workspace with ≥6 months of appointments, deposits, and attendance data across multiple services and time zones, When the user selects a time filter (Last 7/30/90 days, MTD, QTD, YTD, custom range), Then the dashboard recalculates and renders within 2 seconds per interaction. - Given the selected time filter and account timezone, When metrics are computed, Then show-rate = attended / (scheduled − provider-canceled) and is displayed to 1 decimal place with a delta vs. prior period. - Given the selected time filter, When deposit outcomes are computed, Then conversion rate = count(deposits applied to attended sessions) / count(deposits captured) and forfeiture rate = count(deposits forfeited by policy) / count(deposits captured). - Given the selected time filter, When revenue protected is calculated, Then revenue protected = sum(forfeited_deposit_amount in org currency) and is displayed with currency formatting and 2 decimals. - Given the user changes the filter or grouping, When results update, Then the "Last refreshed" timestamp (in account timezone) updates and reflects data freshness ≤15 minutes. - Given no data matches the selected filters, When the dashboard renders, Then empty states with "No data" placeholders appear and metrics show "—" without errors.
Cohort Comparison by Policy Version
- Given at least two policy versions exist with overlapping date ranges, When the user selects Cohort A: Policy v1 and Cohort B: Policy v2 with the same time window, Then side-by-side KPIs render for show-rate, conversion, forfeiture, and revenue protected with percent and absolute deltas. - Given two cohorts are selected, When filters (service, risk tier, slot window) are applied, Then the same filters apply to both cohorts and the denominators are consistent. - Given cohort comparison is active, When the user exports cohort results, Then the CSV contains one row per cohort per metric with columns: cohort_key, policy_version, metric_key, value, prior_period_value, delta_abs, delta_pct, filters_json. - Given invalid or non-overlapping cohorts are selected, When the compare action is attempted, Then the UI blocks with a validation message and comparison is not computed.
Drill-Down to Appointment-Level Audit from Any Metric
- Given a user clicks on a metric (e.g., Forfeitures), When the drill-down opens, Then a table lists only appointments contributing to that metric under current filters. - Given the drill-down table, When it loads, Then it includes columns: appointment_id, client_ref, service_name, risk_tier, slot_start_at (tz-adjusted), policy_version, deposit_amount, deposit_status (authorized|applied|forfeited|released), payment_method_last4 (masked), exception_type (if any), actor, event, event_at. - Given the drill-down table, When the user paginates or sorts by any column, Then results remain consistent with the metric count and respond within 2 seconds. - Given the user clicks Export on the drill-down, When the CSV is generated, Then row_count equals the metric numerator and all applied filters are embedded as metadata (filters_json) in the file header or companion manifest.
Average Deposit by Tier, Service, and Time Slot Risk
- Given appointments with captured deposits exist, When the user groups by risk tier, Then the average deposit amount per tier is computed as sum(deposit_amount)/count(appointments with deposit) and displayed with 2 decimals. - Given grouping by Service or Time Slot window (e.g., peak/off-peak) is selected, When applied, Then the grid shows one row per segment with average, min, max, and count columns. - Given multiple groupings (Tier + Service) are applied, When results render, Then a pivot-style table appears and totals reconcile to the overall figures within ±0.01 currency. - Given a segment has fewer than 5 appointments in the period, When results render, Then the count is shown and averages are displayed but client-identifying fields are suppressed in drill-down per privacy rules.
Exception Rates Reporting (Declines, Expirations, Capture Failures)
- Given deposits attempted during the selected period, When exceptions occur, Then the Exceptions card shows rate = count(exception events)/count(deposit attempts) with breakdown by reason (declined, card expired, insufficient funds, network error). - Given the user clicks a reason in the breakdown, When the drill-down opens, Then it lists the underlying attempts with gateway_code, reason_text (normalized), and retry_outcome (succeeded|failed|not retried). - Given the user applies a client or service filter, When the exceptions view updates, Then counts and rates reconcile to the filtered deposit attempts. - Given exports are requested for exceptions, When generated, Then CSV includes columns: attempt_id, client_ref, service_name, risk_tier, attempted_at, amount, reason_category, gateway_code, retry_count, final_status.
CSV Export and Webhook for External BI
- Given any report or drill-down is exportable, When CSV export is requested, Then files are UTF-8, comma-delimited, RFC 4180 compliant, with ISO-8601 timestamps in account timezone and stable header names. - Given large datasets, When export row_count ≤2,000,000, Then the export is chunked into files of 100,000 rows max with a manifest.json describing file order, schema, filters, and totals. - Given an export of 500,000 rows, When initiated, Then the export completes within 10 minutes and provides pre-signed URLs valid for 7 days. - Given export completion, When webhooks are configured, Then a report.export.completed event is delivered within 60 seconds with payload {export_id, report_key, row_count, parts, checksum_md5, filters_json, url(s), expires_at} and an X-SoloPilot-Signature HMAC-SHA256 header. - Given webhook delivery fails, When the endpoint returns non-2xx, Then retries occur with exponential backoff over 15 minutes (max 6 attempts) and are logged. - Given the user downloads the files, When checksums are validated, Then computed MD5 values match checksum_md5 in the payload.
Role-Based Access and Privacy Controls
- Given user roles Admin and Analyst, When accessing Reporting & Risk Insights, Then both can view all dashboards and exports; Practitioner can view aggregated dashboards but cannot access drill-down or export; Staff cannot access the module. - Given a user without Export permission, When attempting to export, Then the action is disabled and server returns HTTP 403 on direct calls. - Given any view or export, When rendered, Then tenant isolation is enforced and no records from other workspaces are returned. - Given drill-down or CSV includes client fields, When the user lacks PII permission, Then client_ref is pseudonymized, and payment method details are masked (last4 only); raw tokens and full PAN are never included. - Given any report is accessed, When the action completes, Then an audit log entry is recorded with user_id, report_key, filters, action (view|export), row_count, and timestamp.

Confirm‑to‑Keep

Sends timed SMS/email check‑ins (e.g., 24h and 2h) with one‑tap Confirm or Reschedule. Unconfirmed appointments auto‑release at your cutoff and offer a fee‑free reschedule within your grace window. Cuts ghosting, keeps calendars accurate, and gives clients a simple path to move plans without back‑and‑forth.

Requirements

Configurable Reminder Cadence & Channels
"As a solo practitioner, I want to set when and how confirmation reminders are sent so that clients get timely nudges without me micromanaging outreach."
Description

Enable workspace and per-service configuration of automated appointment check-ins at customizable intervals (e.g., 48h, 24h, 2h) delivered via SMS and/or email. Provide templating with merge fields (client name, service, start time, time zone, location/join link) and dynamic insertion of one-tap Confirm and Reschedule links. Respect client time zones and workspace quiet hours, support language/localization, and define channel fallbacks (e.g., SMS → email on delivery failure). Include delivery/retry policies, sender identity management, preview/test send, and deliverability tracking. Integrate with SoloPilot automations to trigger follow-ups based on send, delivery, click, and confirm events.

Acceptance Criteria
Workspace-level reminder cadence and channel configuration
Given I am a workspace admin with appropriate permissions And I configure default reminder intervals at 48h, 24h, and 2h before appointment start And I select SMS and Email as active channels When a new appointment is created for any service without a service-level override Then the system schedules three reminders at the configured offsets based on the appointment start time in the client's time zone And each reminder is queued for both SMS and Email And the appointment timeline shows the scheduled reminders with channel, planned send time, and template name
Per-service override and inheritance
Given workspace default reminder cadence and channels are configured And Service A overrides cadence to 24h only and channel to SMS only When an appointment for Service A is created Then exactly one reminder is scheduled at 24h before the appointment start And the reminder is queued only for SMS And any unset configuration for Service A inherits from workspace defaults And appointments for services without overrides continue to use workspace defaults
Template merge fields and one-tap confirm/reschedule links
Given SMS and Email templates include {client_name}, {service_name}, {start_time}, {time_zone}, {location_link}, {confirm_link}, {reschedule_link} And the client has a preferred language and locale When a reminder is sent Then all merge fields resolve with accurate appointment-specific values And the template variant matching the client's preferred language is used, or the workspace default language if unavailable And date and time are formatted per the client's locale and include the time zone abbreviation And the confirm and reschedule links are HTTPS, unique, single-use, and expire at the configured cutoff time And clicking Confirm sets the appointment status to Confirmed and logs a confirm event with timestamp and channel And clicking Reschedule opens the reschedule flow and logs a click event without changing appointment status
Time zone handling and quiet hours compliance
Given the client has time zone America/Los_Angeles And workspace quiet hours are 21:00–08:00 in the client's time zone When a scheduled reminder time falls within quiet hours Then no message is sent during quiet hours And the reminder send is deferred to the first minute after quiet hours that still precedes the appointment start And if no valid send window remains before the appointment, the reminder is skipped and an info entry is recorded in the appointment timeline
Channel fallback with delivery and retry policy
Given SMS is configured as primary and Email as fallback for reminders And the workspace retry policy is set to a maximum of 2 retries with exponential backoff for transient errors When an SMS send fails transiently Then the system retries SMS per the configured policy and logs each attempt When SMS ultimately fails with a non-transient error or exhausts retries Then the system sends the reminder via Email within 5 minutes of the last failed SMS attempt And the audit log records the failure reason, retry attempts, fallback action, and final delivery status per channel
Sender identity selection and enforcement
Given the workspace has a verified SMS sending number and a DKIM-verified email From domain configured When the admin selects these identities for reminder sends and saves settings Then outbound SMS uses the selected phone number as the sender And outbound Email uses the selected From name and domain and sets the configured Reply-To And unverified sender identities cannot be selected and prevent saving with a clear validation message And each sent message stores the sender identity used in delivery logs
Preview, test send, and event tracking with automations
Given an admin is editing a reminder template When they click Preview Then a preview renders with sample appointment data and shows per-channel character/segment count and estimated cost if available When they click Test Send and provide a destination address/number Then the system sends a test message marked as Test that uses sandbox tokens which do not modify appointment state on click And send, delivery, and click events from Test messages are logged but do not trigger automations And for production sends, send, delivery, click, confirm, and reschedule events are logged with timestamps and appointment ID and trigger the corresponding SoloPilot automations exactly once per event type
Magic‑Link Confirm/Reschedule UX
"As a client, I want a one-tap way to confirm or reschedule from my phone so that I can manage my appointment quickly without logging in or messaging back and forth."
Description

Generate secure, expiring magic links embedded in reminders that let clients confirm or initiate rescheduling without login. Present a mobile-first micro-landing with two primary actions (Confirm, Reschedule), a short contextual message, and optional note/reason capture. Confirmation updates the appointment state immediately and offers add-to-calendar. Reschedule opens a guided flow that surfaces the provider’s next best availability per service/location rules, prevents conflicts, and writes back the new time on selection. Enforce HMAC-signed tokens, single-use, and expiration at cutoff; meet WCAG 2.1 AA; maintain p95 < 1s page load; and log all events for auditability.

Acceptance Criteria
One‑tap Confirmation via Magic Link
Given a valid, unexpired, unused magic link for appointment A When the client taps Confirm Then the appointment status updates to Confirmed within 1 second of tap and is visible in the provider calendar immediately And an in-page success state displays with Add to Calendar options (ICS download and native intents) reflecting correct title, time, timezone, and location And a confirmation SMS/email is sent to the client within 60 seconds And the magic link is invalidated (single-use) And a repeat tap or page reload is idempotent (no duplicate notifications/logs, final state remains Confirmed) And the action is audit-logged with appointment_id, client_identifier, action=confirm, outcome=success, timestamp, user_agent, ip
Reschedule via Magic Link — Guided Flow
Given a valid, unexpired, unused magic link for appointment A When the client taps Reschedule Then a mobile-first flow opens and displays the provider’s next best availability per service, location, buffer, and provider working-hours rules And times that conflict with existing bookings/holds are disabled and cannot be selected And all times are presented in the client’s detected timezone with a visible timezone label and a control to change timezone When the client selects a slot and confirms Then appointment A updates to the new time and the previous time is released immediately And a reschedule confirmation SMS/email is sent within 60 seconds And the updated calendar file/intent reflects the new time And the magic link is invalidated (single-use) And the action is audit-logged with appointment_id, old_time, new_time, action=reschedule, outcome=success, timestamp, user_agent, ip
Magic Link Security — HMAC, Expiration, Single‑Use
Given a magic link is generated for appointment A and client C Then the token is HMAC-SHA256 signed with a server-held secret and encodes appointment_id, client_identifier, expiry, nonce And the token expires at the provider-defined confirmation cutoff timestamp for A And the token is single-use; upon first successful confirm or reschedule it is invalidated When a token is expired, reused, or signature validation fails Then the landing shows an invalid/expired state without exposing appointment details and prevents confirm/reschedule actions And, if within the grace window, a fee-free reschedule option is offered; otherwise guidance to contact/Book Again is shown And invalid attempts are rate-limited and audit-logged with action=token_validate, outcome=denied, reason
Mobile-first Micro‑landing and Accessibility (WCAG 2.1 AA)
Given the micro-landing is opened on a mobile device Then two primary CTAs (Confirm, Reschedule) are visible above the fold with hit targets ≥ 44x44 px and a short contextual message And an optional Note/Reason field is available and non-blocking And the page meets WCAG 2.1 AA: contrast ≥ 4.5:1, keyboard navigable with visible focus, labels correctly associated, appropriate landmarks/roles, screen-reader announcements for dynamic updates, supports 200% zoom without loss of content or functionality, and honors prefers-reduced-motion And all interactive elements are operable by keyboard and touch And automated accessibility checks (axe-core) report 0 critical violations
Performance — p95 < 1s Page Load
Given production conditions approximating 4G (≈150 ms RTT, 1.6 Mbps) and a cold cache Then the micro-landing achieves p95 Largest Contentful Paint < 1.0 s over a rolling 7-day window And p95 Time To First Byte < 300 ms And first-load transfer size ≤ 300 KB and ≤ 10 HTTP requests And real-user monitoring (RUM) and server metrics are captured and visualized with alerts when thresholds are exceeded
Cutoff Enforcement, Auto‑Release, and Grace Reschedule
Given appointment A has a confirmation cutoff time T and a grace reschedule window G When a client opens the magic link after T and A is not confirmed Then Confirm is disabled and the page shows an expired state And appointment A is auto-released so the time slot becomes available in the provider’s calendar And if now is within G, a fee-free reschedule flow is offered; otherwise, the user is guided to alternative options And the view and outcome are audit-logged with action=cutoff_enforced, outcome=expired
Event Logging and Auditability
Given any key event (link_sent, link_opened, confirm_start, confirm_success/fail, reschedule_start, slot_selected, reschedule_success/fail, expired_view) Then an audit entry is recorded with correlation_id, appointment_id, client_identifier, channel (SMS/email), action, outcome, timestamp (ISO-8601), and error_code (if any) And entries are immutable, retained per policy (≥ 12 months), and queryable by appointment_id, client_identifier, correlation_id And an admin can export a chronologically ordered audit report for a single appointment including all related events
Auto‑Release at Cutoff & Waitlist Fill
"As a provider, I want unconfirmed bookings to auto-release at my chosen cutoff so that my calendar stays accurate and open slots can be filled by other clients."
Description

At a configurable cutoff (e.g., 12h before start), automatically release any appointment still unconfirmed: update state to Released, free the time on the calendar, and send notices to client and provider. Offer the released time to the waitlist in priority order with hold timers and auto-book on acceptance; otherwise, return the slot to general availability. Handle edge cases (late confirmations after cutoff, overlapping holds, provider blocks) with deterministic rules and comprehensive audit logs. Ensure updates propagate to external calendars and that downstream automations (e.g., reminders, invoicing) respond to the new state.

Acceptance Criteria
Auto-release at cutoff updates appointment and frees calendar
Given an appointment with start time T, cutoff configured to 12 hours, and status "Scheduled" and "Unconfirmed" When current time equals T - 12h Then the system updates the appointment status to "Released" and records release_timestamp And the provider calendar shows the time slot as free within 60 seconds And client and provider receive release notifications via configured channels within 60 seconds And the appointment is excluded from all future reminder automations
Waitlist priority offering with hold and auto-book
Given a released slot of duration D, a waitlist ordered by priority P1 > P2 > P3, and hold_duration configured to 15 minutes When the slot is released Then the system sends an offer to P1 with a 15-minute hold and marks the slot as "Held (P1)" And if P1 accepts within 15 minutes, the system auto-books P1, sets status to "Scheduled", and sends confirmations to client and provider And if P1 declines or the hold expires, the system offers to the next candidate in priority order with a new 15-minute hold And if no candidates accept, the slot is returned to general availability on the booking page within 60 seconds
Late client confirmation after cutoff
Given an appointment was auto-released at the cutoff and the client uses a prior Confirm link after the cutoff When the client attempts to confirm Then the system prevents confirmation and displays "This appointment was released at [timestamp]" And the client is offered fee-free reschedule options within the configured grace window And if the slot has been rebooked, the client is shown the next available times And the original appointment remains in "Released" state with no reinstatement
Concurrent acceptances and overlapping holds resolution
Given two waitlist candidates attempt to accept the same held slot within 5 seconds When the first acceptance is persisted Then the booking is committed atomically for the first responder and the slot transitions to "Scheduled" And the second acceptance receives a "slot no longer available" message and no booking is created And all other active holds for that slot are immediately canceled and notified And no duplicate events exist on the provider calendar or any external calendars
Provider block or external busy time prevents re-offer
Given the provider has a manual block or an external calendar busy event overlapping the released slot When the original appointment is auto-released Then the system does not send waitlist offers and does not publish the slot to general availability And the provider calendar continues to show the time as busy And client and provider notifications indicate the appointment was released but the time is unavailable
External calendar propagation on release and rebook
Given the provider has Google or Outlook sync enabled When an appointment is auto-released Then the corresponding external calendar event is canceled or removed within 5 minutes And when a waitlist candidate is auto-booked into the slot, a new external event is created with correct attendee, title, and location details within 5 minutes And on sync failure, the system retries at least 3 times and alerts the provider after the final failure
Audit logs and automation reactions
Given an auto-release, subsequent waitlist offers, and any auto-book occur When the sequence completes Then the audit log records each action with actor, timestamps, previous and new states, reason codes, notification types, and recipient identifiers And all reminder workflows for the released appointment are canceled within 60 seconds And invoicing and charge automations are suppressed for the released appointment And if auto-book occurs, a new reminder schedule is created for the new appointment without duplicating messages
Grace Window & Fee Rules Engine
"As a business owner, I want clear, automated grace and fee rules so that clients can reschedule fairly while late cancellations are billed consistently without manual work."
Description

Provide policy controls defining a fee-free reschedule grace window tied to release/appointment times (e.g., up to 12h after release or until 2h before start), and apply fees outside the window per service/client rules. Integrate with invoicing to automatically add late-cancel/no-show fees, discounts, or waivers, with clear client-facing messaging in reminders and landing pages. Support per-client overrides, promo/waiver codes, and an approval workflow for exceptions. Include a simulator to test policies against sample scenarios and an audit trail of decisions and fee applications.

Acceptance Criteria
Grace Window Boundary Calculation
Given an appointment with start time T and release time R in the service time zone and a policy "fee-free until min(R + 12h, T - 2h)" When a client attempts to reschedule at timestamp X Then the system computes window_end = min(R + 12h, T - 2h) and marks the attempt fee-free if X <= window_end, otherwise fee applies And daylight saving and time zone offsets are correctly handled using the appointment time zone And for T=2025-10-10 14:00 and R=2025-10-09 14:00, window_end=2025-10-10 02:00; reschedules at 01:59 are fee-free and at 02:01 incur fees
Fee Assessment and Client Messaging
Given service rules define late-cancel and no-show fees and a grace window policy is active When a client cancels or reschedules outside the grace window Then the correct fee type is selected based on action and timing and the fee amount is computed per service rules And the client reminder/landing page displays the cutoff, whether a fee applies, and the exact fee amount before the client confirms And within the grace window, the page indicates "fee-free" and shows no fee amount And when the appointment auto-releases at cutoff, the landing page offers fee-free reschedule within the remaining grace window; after the window elapses it updates to show the applicable fee
Per-Client Overrides and Precedence
Given a client-specific override exists for grace length or fee amounts for a particular service When evaluating eligibility and fees for that client and service Then the client override supersedes the service default for overridden parameters And if no client override exists for a parameter, the service default is used And per-service client overrides affect only the targeted service; other services use their own defaults or overrides And precedence order is: approved exception > promo/waiver code terms > client override > service default
Promo and Waiver Codes Application
Given a promo/waiver code with defined scope (global/service/client), discount type (percent/fixed/waive), usage limits, activation, and expiry When a client enters the code on the landing page or an admin applies it Then the code is validated for scope, status, usage limits, and time window; if valid, the discount/waiver is applied to the computed fee And the same code cannot be applied more than once per appointment And invalid, expired, or exhausted codes are rejected with a clear reason message And the applied discount appears on the invoice line item and in the audit trail with the code identifier
Exception Approval Workflow
Given the policy requires approval for fee waivers or adjustments outside defined rules When a user submits an exception request with reason and target appointment Then an approver with appropriate role can approve or reject; approvals expire after the configured TTL if unused And upon approval, the fee is waived or adjusted per decision, client messaging updates accordingly, and the invoice reflects the change And all request and decision details (actor, timestamps, reason, outcome) are logged and linked to the appointment
Policy Simulator Decisioning
Given a user opens the simulator and inputs policy version, client, service, release time, start time, action type, and action timestamp When the simulation runs Then it outputs fee-free eligibility, computed fee amount (if any), grace window end time, applied overrides/codes/approvals, and expected invoice line items And it displays the rule decision path with ordered steps and values used And users can save, share, and rerun scenarios; simulations do not create or modify production invoices or records
Invoicing Integration and Audit Trail
Given a fee is determined for an appointment action When generating or updating the invoice Then an itemized line is created with a clear label (type, service, date), amount, tax treatment per service settings, and applied discounts, without duplicating fees for the same appointment action And the invoice line links to a decision record ID for traceability And the audit trail records immutable entries with policy version, evaluated inputs, outputs, client-facing messages, notifications, user IDs, timestamps, and links to invoice/approval/promo; entries are searchable and exportable to CSV
Calendar State Sync & ICS Updates
"As a provider, I want appointment states to stay in sync across all my calendars so that holds, confirmations, and changes are reflected accurately everywhere."
Description

Introduce explicit appointment states (Tentative—awaiting confirm, Confirmed, Released, Rescheduled, Cancelled) and ensure they propagate to SoloPilot calendars and external integrations (Google/Microsoft). Maintain a soft hold during Tentative with configurable block behavior, and update/replace ICS invites on confirm/reschedule/release with accurate summaries, locations, and join links. Guarantee idempotent updates, conflict detection/resolution, and time zone correctness. Provide safeguards on provider disconnects and backfill sync on reconnection to prevent duplicates or stale holds.

Acceptance Criteria
Tentative Soft Hold Blocking Behavior
Given a new appointment is created via Confirm‑to‑Keep When its state is Tentative Then SoloPilot marks the slot as Tentative on the provider calendar And if the Hold blocks availability setting is On, the event exports to external calendars as busy (TRANSP:OPAQUE) and the provider’s availability is blocked And if the Hold blocks availability setting is Off, the event exports to external calendars as free (TRANSP:TRANSPARENT) while remaining visible in SoloPilot And the Tentative hold expires at the configured cutoff if not confirmed, transitioning to Released And upon release, the external calendar event is removed or marked canceled within 60 seconds and the slot becomes available
State Transitions Propagate to SoloPilot and External Calendars
Given an appointment exists in Tentative state When the client confirms, reschedules, releases, or cancels via Confirm‑to‑Keep Then the appointment state updates in SoloPilot within 1 second And a corresponding external calendar mutation occurs within 60 seconds using the same UID: create/update for Confirmed/Rescheduled, cancel/delete for Released/Cancelled And the external event’s summary, location, and join link reflect the latest SoloPilot values after each change And no duplicate external events exist for the same SoloPilot appointment UID
ICS Content and Updates on Confirm/Reschedule/Release
Given an appointment with a stable iCalendar UID is sent to an external calendar When the appointment is confirmed Then an ICS REQUEST is issued with the same UID, SEQUENCE incremented by 1, correct SUMMARY, LOCATION (physical or video link), DESCRIPTION, DTSTART/DTEND with TZID, and VTIMEZONE included When the appointment is rescheduled Then the ICS REQUEST preserves UID, increments SEQUENCE by 1, updates DTSTART/DTEND and any join links, and the external event moves to the new time When the appointment is released or cancelled Then an ICS CANCEL is issued preserving UID and the external event is removed/marked cancelled within 60 seconds
Idempotency and Duplicate‑Prevention in Sync Operations
Given the sync service receives duplicate webhooks or performs retries for the same appointment version When the identical update (same appointment UID and SEQUENCE/version) is processed multiple times Then only one external calendar mutation occurs and no duplicate events are created And reprocessing with the same SEQUENCE results in a no‑op, while higher SEQUENCE applies exactly once And end‑to‑end retries complete without generating additional holds or stale entries in SoloPilot or external calendars
Conflict Detection and Policy‑Based Resolution
Given an appointment is being confirmed or rescheduled to a slot that conflicts with an existing busy event (internal or imported from external) When the provider’s conflict policy is Prevent Overbooking Then the operation is blocked, the state remains Tentative, the user receives a conflict error, and the client is prompted to choose a different time When the provider’s conflict policy is Allow Overbooking Then the operation completes, the appointment is set to Confirmed with an Overbooked flag, and a notification is logged to the provider And in all cases, external calendar updates reflect the resolved state only (no partial updates are sent on failed confirmations)
Time Zone Accuracy Across DST and Regions
Given an appointment is scheduled in the provider’s time zone and crosses a DST change When viewed in SoloPilot and in Google/Microsoft calendars Then the local start/end times match across systems, with ICS including correct TZID and VTIMEZONE, and no one‑hour drift And when a client confirms/reschedules from a different time zone, the stored UTC timestamps are unchanged and both parties see correct local times And rescheduling between time zones preserves absolute UTC time and adjusts displayed local times accordingly
Disconnect Safeguards and Backfill Sync on Reconnection
Given a provider disconnects Google/Microsoft calendar When appointment states change (Tentative, Confirmed, Rescheduled, Released, Cancelled) during the disconnect Then no external API calls are attempted and changes are queued for backfill When the provider reconnects Then the last 30 days of appointments are reconciled within 10 minutes without duplicates: current Confirmed/Rescheduled events are upserted, Released/Cancelled events are removed/cancelled, and stale external holds are cleared And a sync log shows per‑appointment outcomes (success/failure) for audit
Consent, Compliance, and Opt‑Out Controls
"As an operator, I want built-in consent and opt-out enforcement so that my reminders are compliant and respect client preferences automatically."
Description

Collect and store explicit per-channel consent for SMS and email, enforce region-specific quiet hours and sender IDs, and include mandatory disclosure/opt-out instructions (STOP/UNSUBSCRIBE) in every message. Honor opt-outs immediately and route reminders to an allowed channel when one is blocked. Log consent capture, message content, sends, deliveries, clicks, and user actions for compliance (e.g., TCPA, CAN-SPAM, GDPR) with exportable audit records. Surface consent status in the client profile and prevent sends when compliance conditions are unmet.

Acceptance Criteria
Per‑Channel Consent Capture and Audit Logging
Given a new client signs up via SoloPilot and is presented with separate SMS and Email consent checkboxes set to unchecked by default, When the client explicitly checks one or both and submits, Then the system stores per‑channel consent with timestamp (ISO‑8601), source (signup form), client IP, region, and disclosure policy version. Given a client grants consent via channel‑specific flows (e.g., SMS keyword YES, email double opt‑in link), When the confirmation is received, Then the system stores per‑channel consent with capture method (e.g., SMS keyword, Email DOI), message/template ID, and user agent if applicable. Given any consent status changes (grant or revoke), When the change is saved, Then an immutable audit entry is appended capturing previous value, new value, actor (client/user/system), reason, and timestamp without overwriting prior history. Given an admin requests a compliance export for a date range and client cohort, When the export is generated, Then the file includes consent records, message content snapshots, send/delivery/open/click/response events, and user actions with a SHA‑256 checksum and timezone context in CSV and JSON formats. Given a channel has no recorded consent, When Confirm‑to‑Keep attempts to schedule a send on that channel, Then the send is suppressed and the suppression reason is recorded as "No Consent" in the audit log.
Mandatory Disclosures and Opt‑Out Instructions in Every Message
Given an SMS reminder is generated, When the message is compiled, Then it includes the business name, purpose, frequency statement, "Msg&data rates may apply", and "Reply STOP to opt out" in the final SMS body within length limits. Given an email reminder is generated, When the message is compiled, Then it includes accurate From/Sender identity, physical mailing address in the footer, a one‑click unsubscribe link, and list identity, and the subject line is not deceptive. Given a message template lacks required disclosures or opt‑out instructions for the recipient’s region, When a send is attempted, Then the system blocks the send, surfaces a validation error specifying the missing element(s), and logs the failure. Given regional variations in mandatory language are configured, When messages are sent to recipients in those regions, Then the system inserts the region‑appropriate disclosure copy automatically. Given a user edits templates, When they save, Then a compliance validator runs and prevents saving noncompliant templates with actionable error messages.
Immediate Opt‑Out Processing (STOP/UNSUBSCRIBE)
Given a client replies with an industry‑standard opt‑out keyword (STOP, STOPALL, UNSUB, CANCEL, END, QUIT) to the SMS number used, When the inbound message is received, Then the SMS channel for that client is immediately marked Opted‑Out and all future SMS sends are suppressed. Given an SMS opt‑out is processed, When confirmation is permitted by policy, Then the system sends a single confirmation SMS acknowledging the opt‑out and provides instructions to re‑opt‑in (e.g., reply START), and no further messages are sent afterward. Given a client clicks an email unsubscribe link, When the unsubscribe request is recorded, Then the client’s Email channel is marked Opted‑Out within 60 seconds and subsequent emails are suppressed. Given any opt‑out occurs while a message is queued but not yet delivered, When the send queue runs, Then the queued message on that channel is canceled and the cancellation is logged with reason "Opt‑Out Received". Given a client re‑opts in via START (SMS) or resubscribe (Email), When confirmed, Then the channel status updates to Opted‑In with new timestamp and method recorded, and future sends may proceed subject to other compliance checks.
Region‑Specific Quiet Hours Enforcement
Given quiet hours are configured per region and channel, When a reminder is scheduled during the client’s local quiet hours, Then the system does not send and automatically reschedules to the earliest permitted time window, recording the reschedule in the audit log. Given a client’s timezone is available in their profile, When enforcing quiet hours, Then the client profile timezone is used; else if absent, the timezone is inferred from phone country/area code or email locale mapping and recorded in the log. Given a user attempts a manual override send during quiet hours, When the policy forbids overrides, Then the send is blocked with a clear explanation of the quiet hours window and next available send time. Given different quiet hour rules exist for SMS and Email in a region, When evaluating a scheduled send, Then the system applies the channel‑specific rule to determine eligibility. Given a reminder is deferred due to quiet hours, When the new send time is set, Then the appointment’s confirm‑by cutoff logic remains intact and is recalculated against the new send time if applicable.
Region‑Specific Sender ID Compliance
Given a reminder will be sent to a country requiring registered sender IDs, When the message is dispatched, Then the system uses the correct registered long code/short code/alphanumeric sender configured for that country. Given two‑way reply handling is required for the flow, When selecting a sender, Then the system assigns a two‑way capable number in that region and logs the selected sender ID. Given no compliant sender ID is available for the recipient’s region, When a send is attempted, Then the system blocks the send, surfaces an error specifying the missing registration, and records the failure with remediation guidance. Given a message is delivered, When logging the event, Then the sender ID actually used is captured in the delivery log for auditability. Given sender ID mappings are updated by an admin, When changes are saved, Then validation ensures no region is left without a compliant sender for enabled channels and a change audit entry is recorded.
Channel Routing When a Channel Is Blocked
Given an SMS reminder is due but SMS is Opted‑Out or blocked by quiet hours, When Email is consented and allowed, Then the reminder is routed to Email within the same schedule window and includes equivalent Confirm/Reschedule actions. Given an Email reminder is due but Email is Opted‑Out or suppressed (e.g., bounces, complaints), When SMS is consented and allowed, Then the reminder is routed to SMS and the routing decision is logged with the suppression reason for Email. Given both SMS and Email are blocked or lack consent, When a reminder is due, Then no message is sent, the suppression reason is logged per channel, and the appointment remains governed by auto‑release rules without sending. Given routing occurs for a specific reminder instance, When evaluating duplicates, Then exactly one channel is used per reminder instance and duplicate sends across channels are prevented. Given a routing decision is made, When viewing the message log, Then the log shows the original channel intent, the final channel used, and the reason code for any fallback.
Consent Status UI and Pre‑Send Compliance Gate
Given a user opens a client profile, When viewing communications settings, Then per‑channel consent status (Opted‑In/Opted‑Out/Unknown), last updated timestamp, capture method, region, and quiet‑hours policy tag are displayed. Given a user attempts to send a manual reminder or the system schedules one, When the pre‑send compliance gate runs, Then it validates consent presence, opt‑out status, quiet hours, sender ID availability, and disclosure/template compliance before allowing the send. Given any validation fails, When the send is blocked, Then the UI/API returns a structured list of reason codes (e.g., NO_CONSENT_SMS, QUIET_HOURS_EMAIL, MISSING_DISCLOSURE), and the event is logged. Given a bulk send is prepared, When previewing, Then the UI shows counts by allowed vs suppressed with filters by suppression reason and offers export of the suppressed list. Given the compliance gate allows a send, When the send proceeds, Then the gate decision and the rules evaluated are recorded in the audit log for traceability.
Confirmation Analytics & A/B Testing
"As a solo practice owner, I want visibility into what reminder cadences work best so that I can reduce no-shows and maximize booked time."
Description

Provide dashboards and reports for send, delivery, click, confirm, reschedule, and release rates segmented by service, channel, cadence, and client cohort. Attribute changes in no-shows, utilization, and revenue to the feature. Enable A/B testing of send times and copy with statistically sound sample sizing, and recommend best-performing cadences. Offer CSV export and webhook/event streaming for BI tools and automations, with privacy-safe aggregation and retention controls.

Acceptance Criteria
Dashboard Segmentation & Drilldown
Given a workspace with at least 90 days of Confirm-to-Keep events across multiple services, channels (SMS, email), cadences (e.g., 24h, 2h), and client cohorts (e.g., new vs returning) When a user applies any combination of filters (date range, service, channel, cadence step, cohort, timezone) Then the dashboard returns send, delivery, click, confirm, reschedule, and release counts and rates with definitions: delivery_rate = delivered/sent, click_rate = clicks/delivered, confirm_rate = confirmations/sent, reschedule_rate = reschedules/sent, release_rate = releases/scheduled And results load within 2 seconds for datasets up to 1,000,000 events and show a Data last updated timestamp not older than 15 minutes And drilldown from service to appointment level is available; clicking a row loads the next level within 3 seconds and masks PII per privacy controls And empty or missing data displays as N/A and is excluded from rate denominators
A/B Testing: Send Times and Copy
Given an experiment with variants that change send_time and/or message_copy and a chosen primary metric confirm_rate When the experiment is launched Then randomization occurs at client_id level, stratified by service, with no client assigned to more than one active variant for the same appointment And minimum sample size per variant is auto-calculated for 80% power at alpha = 0.05 using the specified baseline; start is blocked if projected traffic cannot reach sample size within the selected duration And the system will not declare a winner until the minimum sample size is met; after completion it reports winner, observed lift, 95% CI, p-value, and variant metadata And the user can pause, resume, or stop the experiment; all changes are audit-logged with timestamp and actor And experiment results are archived and filterable; metrics match dashboard segment totals within 0.1%
Causal Attribution to No-Shows, Utilization, and Revenue
Given a persistent holdout control of at least 10% of eligible appointments per service and channel When attribution is computed for a selected period Then the system uses difference-in-differences vs the holdout to estimate attributable changes in no-show rate, utilization (booked_hours/available_hours), and revenue (invoiced_paid), reporting point estimate, 95% CI, and p-value And results are suppressed if the control group has <100 appointments or any slice violates k-anonymity (k<10 clients) And revenue attribution reconciles to invoices paid within the period and currency; exchange rates are fixed to the transaction date And attribution recalculates daily at 02:00 workspace timezone and shows last run timestamp
Cadence Recommendations
Given at least one completed experiment or 30 days of production data meeting sample thresholds per service/channel When evaluating candidate cadences Then the system recommends the cadence with the highest confirm_rate subject to non-inferiority on release_rate and no-show rate (delta <= 0.5pp) with 95% CI And each recommendation displays expected lift, CI, data volume, last evaluated time, and the key drivers (send_time, copy length, channel mix) And recommendations are only surfaced when power >= 0.8 and p-value <= 0.05; otherwise an insufficient evidence message is shown And an Apply action updates the service default cadence, schedules activation start time, and writes an audit log entry; rollback is available
CSV Export & Webhook/Event Streaming
Given any dashboard view with filters applied When the user triggers a CSV export Then a UTF-8 CSV is generated within 60 seconds containing a header and rows with: date/time (ISO-8601, workspace timezone), service, channel, cadence_step, cohort, sent, delivered, clicks, confirmations, reschedules, releases, and computed rates; nulls are empty fields; up to 5,000,000 rows via chunked download And exported totals match on-screen totals within 0.1% for the same filters and time zone And when webhooks are enabled, events (send, delivered, clicked, confirmed, rescheduled, released) are POSTed within 5 seconds average with at-least-once delivery, idempotency key, HMAC-SHA256 signature, retry with exponential backoff up to 24 hours, and preserved order per appointment_id And webhook payloads exclude PII, include stable IDs and timestamps, and are documented via machine-readable schema; delivery failures are visible in an admin log
Privacy-Safe Aggregation & Retention Controls
Given workspace privacy settings When k-anonymity is set to 10 and retention is set to 90 days Then any metric slice with fewer than 10 distinct clients is suppressed (displayed as <k) and excluded from exports and webhooks And raw event data older than 90 days is purged automatically; manual purge and right-to-be-forgotten delete client events and derived aggregates within 7 days And access to exports, webhooks, and attribution is restricted to Owner/Admin roles; all access is audit-logged with user, time, and action And phone numbers, emails, and message bodies are masked or redacted in UI, exports, and events; IP addresses are not stored

Waitlist Backfill

When a slot opens or lapses confirmation, SoloPilot auto‑pings your prioritized waitlist with a one‑tap Claim link that collects the deposit instantly. Fills gaps within minutes, preserves your billable time, and fairly rotates invites so frequent waitlisters get timely chances.

Requirements

Real-time Slot Detection & Backfill Trigger
"As an independent practitioner, I want openings to auto-trigger a backfill workflow so that my calendar stays full without me intervening."
Description

Continuously monitor the SoloPilot scheduling engine for newly opened availability caused by cancellations, reschedules that free a slot, or expired confirmations. When a qualifying slot is detected (respecting service type, duration, location, practitioner availability, prep buffers, and lead-time thresholds), automatically initiate a backfill workflow. Ensure idempotent triggers, timezone awareness, and guardrails to avoid duplicate campaigns for the same slot. Integrate with existing calendar/automation services so backfill runs without manual intervention and logs all events for traceability. Outcome: qualifying openings are converted into actionable backfill campaigns within seconds, maximizing utilization and preserving billable time.

Acceptance Criteria
Freed slot (cancel/reschedule) triggers backfill within seconds
Given a confirmed appointment for Service "Coaching 60" with Practitioner P at Location L from 15:00-16:00 And a configured prep buffer of 10 minutes and lead-time threshold of 2 hours And the lead-time and buffer requirements are satisfied When the appointment is cancelled OR rescheduled off the 15:00-16:00 slot at time T Then the slot 15:00-16:00 becomes available respecting buffers And within 10 seconds a single backfill campaign is created for that slot And the campaign payload includes serviceId, practitionerId, locationId, slotStart, slotEnd, reason ("Cancellation" or "Reschedule"), leadTimeMinutes, bufferSatisfied = true And the campaign status is set to "Pending Outreach"
Expired confirmation hold triggers backfill
Given a tentative booking for Service "Therapy 50" holding slot 10:00-10:50 with auto-expire at 09:00 and lead-time threshold of 1 hour And the client does not confirm before 09:00 When the confirmation hold expires at 09:00 Then the slot 10:00-10:50 is released to available respecting buffers And if lead-time and buffers are satisfied, within 10 seconds a single backfill campaign is created with reason "ExpiredConfirmation" And if lead-time is not satisfied, no backfill campaign is created and a "NotEligible" evaluation event is logged with reason "LeadTime"
Eligibility filter enforces service, duration, location, practitioner, buffers, and lead-time
Given a candidate freed slot When evaluating backfill eligibility Then the system must require: - serviceType is marked BackfillEnabled - slot duration equals service.requiredDuration - locationId is in service.allowedLocations - practitioner P is assigned to the service, not out-of-office, and has no conflicting events - prep buffers before and after are satisfied relative to adjacent events (including travel buffers if enabled) - now until slotStart >= service.leadTimeThreshold And if any requirement fails, no campaign is created and the evaluation result records all failing reasons
Idempotent trigger and duplicate guardrails
Given the same freed slot emits multiple events (e.g., cancellation webhook, calendar sync, manual update) within 60 seconds When the backfill workflow processes these events concurrently Then an idempotency key computed from {practitionerId, serviceId, locationId, slotStart, slotEnd} is used to deduplicate And at most one backfill campaign is created for the slot And subsequent duplicate attempts return the existing campaignId without side effects And the audit log records dedupeOutcome = "hit" for duplicates and "miss" for the first
Timezone- and DST-safe detection
Given the account default timezone is UTC and the practitioner's calendar timezone is America/Los_Angeles And a slot falls on a DST transition day (e.g., 2025-03-09 at 01:30 local) When a qualifying freeing event occurs Then slot availability calculations use the calendar's timezone rules (including DST skips/repeats) And lead-time comparisons are performed in the practitioner's local time And the campaign payload includes ISO-8601 UTC timestamps and localTime with timezone offset And no off-by-one-hour errors occur in stored or emitted times
Integration and resilient retries without duplicates
Given calendar update and automation enqueue endpoints are reachable When a qualifying slot is detected Then the system updates availability, persists campaign, and enqueues the backfill job, each returning 2xx And if any downstream call fails with 5xx or timeout, the system retries with exponential backoff up to 5 attempts over 15 minutes And retries do not create duplicate campaigns due to idempotency key reuse And failure after the final attempt sets campaign status = "Errored" and emits an alert event "BackfillTriggerFailed"
Comprehensive audit logging and traceability
Given a qualifying slot detection from any reason When the backfill trigger runs Then a structured log record is persisted with fields: correlationId, slotId, reason, serviceId, practitionerId, locationId, slotStart, slotEnd, leadTimeMinutes, bufferSatisfied, idempotencyKey, campaignId, outcome, latencyMs And logs are written before acknowledging external webhooks to ensure traceability And logs are retained for at least 365 days and are queryable by correlationId and slotId
Fair Waitlist Prioritization & Rotation Engine
"As a coach, I want my waitlist invites to be fairly rotated and prioritized so that clients feel treated equitably and I reliably fill gaps."
Description

Implement a deterministic, auditable prioritization engine that ranks waitlisted clients using configurable rules: request recency, historical responsiveness, tenure, no-show history, service fit, and stated availability. Enforce fair rotation so frequent waitlisters receive timely opportunities while preventing leapfrogging and gaming (cooldowns, per-user invite caps, and tie-breakers). Support per-slot filtering (service, location, duration) and exclusions (blocked clients, policy violations). Expose ordering rationale in the admin UI for transparency and provide admin overrides when necessary. Outcome: equitable, policy-aligned selection of invitees that increases fill rates and customer trust.

Acceptance Criteria
Deterministic, Reproducible Ranking Output
Given a fixed set of waitlist inputs, rule weights, tie-breaker order, and system time frozen, When the engine computes the ranking multiple times, Then the resulting order and per-candidate scores are identical across runs. Given a saved rule-weight configuration with a version identifier, When weights are changed and saved, Then subsequent rankings reference the new version identifier and produce an order consistent with the updated weights. Given no changes to inputs or configuration, When ranking is recomputed later, Then the correlation ID is new but the input snapshot hash and resulting order remain the same.
Per-Slot Eligibility Filtering and Exclusions
Given a slot with defined service, location, and duration, When building the candidate pool, Then only clients matching the service, accepting the location, and with availability overlapping the slot duration are included. Given clients flagged as blocked or with policy violations, When filtering, Then those clients are excluded from the candidate pool with a recorded exclusion reason code. Given a client lacks stated availability for the slot time, When filtering, Then the client is excluded with reason code AVAILABILITY_MISMATCH.
Fair Rotation, Cooldown, and Invite Caps
Given a configured cooldown window and per-user invite cap, When generating an invite list, Then eligible clients who received an invite within the cooldown are ranked after eligible clients without recent invites. Given a client has reached the invite cap within the rolling period, When generating the invite list, Then the client is skipped and annotated with reason code INVITE_CAP_REACHED. Given two consecutive openings with overlapping eligibility, When generating invite lists for both, Then no client receives the top invite position twice in a row if another eligible client exists.
Anti-Gaming Protections Against Leapfrogging
Given a client leaves and re-joins the waitlist within the anti-reset window, When ranking, Then the original join timestamp is used for priority and the cooldown state is retained. Given a client submits multiple requests for the same service group, When ranking, Then duplicates are collapsed into one consideration and do not increase ranking. Given a client edits availability after the notification cutoff for a specific opening, When ranking for that opening, Then the edit does not alter eligibility or position.
Ordering Rationale Transparency in Admin UI
Given a generated invite list, When viewed in the admin UI, Then each candidate displays total score, per-factor contributions, applied penalties, and the tie-breaker path used. Given a generated invite list, When exporting details, Then a correlation ID, configuration version, and input snapshot hash are included. Given a client is excluded, When viewed in the admin UI, Then the exclusion reason code and a human-readable explanation are displayed.
Admin Overrides with Audit and Constraint Enforcement
Given an admin reorders, skips, or adds a candidate before sending invites, When saving, Then user, timestamp, change set, and reason note are recorded and linked to the invite run. Given an admin attempts to invite an excluded or capped client, When saving, Then the system blocks the action and displays a specific error referencing the violated rule. Given overrides are saved for one opening, When a new opening is processed, Then the base ranking engine runs without inheriting prior overrides.
Responsiveness and No-Show History Influence
Given clients with differing acceptance rates and median response times within the configured lookback, When scoring historical responsiveness, Then higher acceptance and faster responses produce higher scores within defined bounds. Given a client with no-show count above the configured threshold, When scoring, Then a penalty is applied that reduces total score according to configuration. Given the lookback window configuration is changed, When ranking is recomputed, Then responsiveness and no-show components use the new window and updated scores are reflected in the rationale.
One‑Tap Claim Link with Instant Deposit Capture
"As a client on the waitlist, I want to claim an opening with one tap and pay the deposit instantly so that I can secure the slot quickly and confidently."
Description

Generate secure, single-use magic links per invitee that open a streamlined claim page pre-populated with slot details, price, deposit terms, and policies. Enable one-tap confirmation by charging the required deposit instantly using saved payment methods or entering new details via PCI-compliant tokenization (e.g., Stripe) with SCA/3DS support where applicable. On successful payment, auto-create the appointment, apply deposit to the invoice, send confirmations/receipts, and update the waitlist/slot state. Handle payment failures gracefully with clear guidance and retry options. Outcome: frictionless conversion from invite to confirmed booking with immediate deposit collection to reduce no-shows.

Acceptance Criteria
Single‑Use Magic Link Generation and Security
Given a provider selects a waitlist invitee for an open slot When SoloPilot generates a claim link Then the link is unique, single-use, bound to the invitee and slot, and expires at the earlier of the provider-configured expiry or 30 minutes by default And the link is immediately invalidated upon slot fill, invite revoke, manual cancel, or after one successful payment And attempts to reuse, forward, or open after expiry return an "Invalid or expired link" page without revealing slot details And the link token has at least 128 bits of entropy and is served over HTTPS And an audit log entry records generator, timestamp, invitee, slot, and current link status
Pre‑Populated Claim Page Accuracy
Given an invitee opens a valid claim link When the claim page loads Then it displays slot date/time in the invitee’s timezone, service name, duration, location/modality, price, deposit amount, remaining balance, deposit terms, cancellation/no‑show policy, and link expiry countdown And all monetary values reflect configured pricing/taxes and are formatted in the provider’s currency And if slot details changed or slot was canceled, the page reflects current status and disables payment with an explanatory message And the page prevents submission until the invitee checks acknowledgment of policies and terms (if required by provider)
One‑Tap Confirm with Saved Payment Method
Given the invitee has at least one saved payment method When the invitee taps Confirm & Pay Then SoloPilot charges the deposit amount immediately via the default saved method using the payment processor And invokes SCA/3DS authentication when required by issuer/regulation And prevents duplicate submissions while the transaction is pending And on successful authorization and capture, proceeds to booking; on failure or cancel, no booking is created and a clear error is shown
New Payment Method Entry with PCI Tokenization
Given the invitee chooses to add a new payment method When card details are entered Then fields are rendered via PCI-compliant tokenized elements and raw card data never touches SoloPilot servers And SCA/3DS is invoked as required; upon approval the deposit is charged immediately using the tokenized method And the invitee can optionally save the method for future use only with explicit consent And common errors (invalid card, insufficient funds, authentication failed) are clearly displayed with guidance, and the invitee can retry with the same or a different method
Post‑Payment Automation and State Updates
Given the deposit is successfully captured When the payment gateway confirms success Then SoloPilot creates the appointment for the claimed slot and marks the slot as filled And updates the waitlist: the invitee is marked Claimed/Booked and others are marked Not Available And creates or updates an invoice applying the deposit as a payment with gateway transaction ID recorded and remaining balance shown And sends booking confirmation to both parties and a payment receipt to the invitee within 1 minute And invalidates all outstanding claim links for the slot And operations are idempotent so retries do not create duplicate appointments, invoices, or messages
Payment Failure Handling and Retry Flow
Given a payment attempt fails or is canceled When the result is shown to the invitee Then the message includes a human-readable reason and next-step guidance without exposing sensitive data And the invitee can retry up to three times within the link validity window, choose a different payment method, or abandon And no appointment is created and the slot remains available to others until a successful payment occurs And analytics/audit logs record failure reason, attempt count, and outcome
Concurrency and Race Conditions Protection
Given multiple invitees attempt to claim the same slot concurrently When payments are processed Then only the first successfully captured payment confirms the slot; subsequent attempts receive a "Slot no longer available" message and are not charged And if a late capture occurs after the slot is taken, SoloPilot auto-voids/refunds the deposit and notifies the payer And gateway idempotency keys and atomic checks prevent duplicate appointments or double charges And all related claim links are invalidated immediately upon confirmation
Multi‑Channel Notifications & Delivery Controls
"As a practitioner, I want SoloPilot to notify my waitlist through the best channel at the right time so that invites are seen and acted on quickly without spamming clients."
Description

Send invite notifications via email, SMS, and (where available) push, using customizable templates and dynamic slot details. Support batched or staged outreach (waves) with configurable batch size, delays, and quiet hours; enforce per-user rate limits and opt-out preferences to maintain trust and compliance. Include link-tracking, per-channel delivery callbacks, retries, and fallbacks; validate phone/email to improve deliverability. Localize content by locale and service. Outcome: reliable, timely outreach that maximizes engagement while respecting user preferences and communication policies.

Acceptance Criteria
Multi-Channel Send with Dynamic Slot Details and One-Tap Claim Link
Given a waitlist slot becomes available and channels {email, SMS, push} are enabled with active templates And the recipient has valid contact methods for those channels When outreach is triggered for the slot Then the system composes each channel message using the configured template and injects dynamic slot details (service name, date, start time, duration, timezone, location/meeting link) And generates a unique, signed Claim URL per recipient with embedded slot and waitlist identifiers and a tracking ID And sends the message via each enabled channel And persists an outreach record per recipient per channel with message ID, tracking ID, channel, and timestamp And link tracking records a click event with tracking ID and channel when the Claim URL is visited and associates it to the recipient And the Claim URL becomes invalid once the slot is no longer available or after its configured TTL
Wave-Based Outreach Respects Batch Size, Delay, and Quiet Hours
Given wave settings are configured with batch_size=N, inter_batch_delay=M minutes, and quiet_hours start/end in the recipient’s timezone And K eligible waitlisters are prioritized When a slot opens and outreach scheduling runs Then the first min(N, K) recipients are scheduled immediately if current time is outside quiet hours, otherwise at quiet_hours_end And subsequent batches of size N are scheduled every M minutes until all K are scheduled or the slot is claimed And no messages are sent during quiet hours And if the slot is claimed, all unsent batches are automatically canceled and no further invites are sent And the schedule and any cancellations are recorded with timestamps
Per-Channel Rate Limits and Opt-Out Compliance
Given per-recipient and per-account rate limits are configured (e.g., max_sms_per_day, max_email_per_hour) and channel opt-out preferences exist per recipient When evaluating whether to send an invite per channel Then the system suppresses sends for any channel where the recipient is opted out or limits would be exceeded And records a suppression entry with recipient, channel, reason, and timestamp And for SMS, a STOP reply updates the recipient’s SMS opt-out within 60 seconds and suppresses any pending/future SMS invites And for email, an unsubscribe link click updates the email opt-out within 60 seconds and suppresses any pending/future email invites And suppressed recipients are skipped in current and future waves for the suppressed channels
Delivery Callbacks, Retries, and Fallback Routing
Given provider delivery callbacks are enabled per channel and retry/fallback policies are configured (retry_max=R, backoff strategy, fallback_enabled, undeliverable_timeout=T) When a send attempt results in a transient failure Then the system retries up to R times with exponential backoff and logs each attempt and outcome When a send attempt returns a hard bounce, invalid number, or revoked push token Then that channel is marked undeliverable for the recipient and no further retries occur on that channel And, if fallback_enabled and an alternate allowed channel exists, one fallback send is attempted respecting rate limits and opt-outs And all provider callbacks (queued, sent, delivered, failed, bounced) are stored with timestamps and correlated to the outreach record And the notification is marked Delivered only upon a Delivered callback or successful fallback delivery within T minutes; otherwise it is marked Undeliverable
Address Validation and Normalization Prior to Send
Given a recipient has email and/or phone contact information When preparing outreach Then the system validates email format and MX records and normalizes phone numbers to E.164 with country/region inference And performs carrier/type lookup for SMS where supported And if validation fails for a channel, no message is queued for that channel and a validation_error is recorded with reason And validated addresses are cached for reuse with a configurable TTL And overall validation outcomes (passed, failed, corrected) are persisted per recipient per channel
Localization by Locale and Service with Safe Fallbacks
Given a recipient has a user locale and the service defines an optional locale override And templates exist in multiple locales When composing content for an invite Then the system selects the template using priority: service override > user locale > account default And renders date/time in the recipient’s timezone using the resolved locale’s formats and translates static strings accordingly And fills dynamic placeholders (service name, date, time, duration, location, claim link) without unresolved tokens And if a template for the resolved locale is missing, the account default template is used And the resolved locale and template version are stored with the outreach record
Claim Window, Holds & Concurrency Resolution
"As a therapist, I want the system to handle simultaneous claims fairly and automatically so that I never end up double-booked or mediating conflicts."
Description

Define a configurable claim time-to-live (TTL) and implement first-paid-wins logic with atomic state transitions to handle simultaneous claims. When an invitee begins checkout, place a short hold on the slot; confirm only upon successful deposit. If the claim window expires, payment fails, or an invitee declines, automatically roll down to the next candidate(s) per rotation rules. Handle edge cases where the original client reconfirms: apply clear precedence rules (e.g., confirmed and paid backfill overrides pending reconfirmations) and communicate outcomes to all parties. Outcome: no double-bookings, predictable behavior under contention, and minimal manual intervention.

Acceptance Criteria
TTL Expiry Rolls Down to Next Candidate
Given an invite with a claim TTL of T minutes is sent to Candidate A And no deposit is completed within T When T elapses Then the slot hold (if any) is released within 5 seconds And Candidate A's invite status becomes Expired And Candidate B is auto-invited per rotation within 30 seconds And the audit log records invite_sent, ttl_expired, roll_down_triggered with timestamps
First-Paid-Wins Under Concurrent Checkout
Given Candidates A and B initiate checkout for the same slot within an active claim window And both receive a hold token for the slot When Candidate A's deposit is authorized and captured first Then Candidate A's booking transitions to Confirmed atomically And Candidate B's payment attempt is rejected prior to capture with error_code=SLOT_TAKEN and HTTP 409 And exactly one Confirmed booking exists for the slot And both sessions are idempotent on retry (no duplicate bookings or charges)
Hold Lifecycle on Checkout Start and Release
Given a candidate taps Claim and reaches the payment page When the system places a hold for H minutes (configured per workspace) Then the slot is unavailable to new invitees except those already in checkout while H is active And if payment succeeds within H, the hold converts to Confirmed and the deposit is captured once And if payment fails or checkout is abandoned, the hold is released immediately and the invite status updates to Payment Failed or Abandoned And if H expires, the hold is released within 5 seconds and the invite status updates to Expired
Explicit Decline Triggers Immediate Roll-Down
Given Candidate A has an active invite for a slot When Candidate A taps Decline Then Candidate A's invite status becomes Declined And the next candidate(s) are invited per rotation within 30 seconds And if no candidates remain, the slot is returned to open availability and the practitioner is notified And all actions are written to the audit log with actor, reason, and timestamps
Precedence: Paid Backfill Overrides Later Reconfirmation
Given the original client has a pending reconfirmation on the slot And a waitlist backfill invite is active When a waitlist candidate completes deposit successfully before the original client reconfirms Then the backfill booking remains Confirmed And any subsequent reconfirmation attempt by the original client is rejected with error_code=SLOT_TAKEN And the original client receives alternatives (reschedule link) and a clear explanation And all pending waitlist sessions except the winner are cancelled
Precedence: Original Client Reconfirms Before Backfill Payment
Given the original client reconfirms before any waitlist deposit is captured When reconfirmation is received Then the slot is Confirmed for the original client And all active waitlist holds and checkout sessions are cancelled with reason=RECLAIMED_BY_ORIGINAL And any in-flight payments are voided before capture and users see error_code=SLOT_RECLAIMED And affected invitees are notified within 60 seconds
Atomicity, Idempotency, and No Double-Booking Guarantees
Given concurrent claim, pay, reconfirm, decline, and expiry events target the same slot When requests are retried with the same idempotency key or arrive out of order Then state transitions use atomic compare-and-set (or transactional) operations And repeated requests with the same idempotency key do not create duplicate holds, charges, or bookings And system invariants hold: at most one Confirmed booking per slot; zero overlapping holds for the same actor And chaos tests with 10,000 simulated concurrent claims produce zero double bookings and ≤0.1% transient payment errors, all auto-reconciled
Admin Configuration, Policies & Audit Trail
"As an owner, I want to configure how backfill works and see an audit of each decision so that I can run my practice my way and resolve disputes quickly."
Description

Provide an admin UI to configure deposit amounts per service, eligible time windows, lead-time thresholds, prioritization rules, batch sizes, channels, templates, quiet hours, and auto-refund/cancellation policies. Allow manual actions (pause/resume a backfill, resend invites, skip/ban a client) with confirmations. Record an immutable audit trail of triggers, rankings, invites, clicks, payments, confirmations, and policy decisions, with timestamps and actor identity, exportable for compliance. Outcome: operators can tune the system to their practice and review exactly what happened for any slot.

Acceptance Criteria
Configure Deposit Amounts Per Service
Given I am an admin on the Waitlist Backfill settings page, When I set the deposit for service "Initial Consult" to 75.00 and click Save, Then the value persists and reappears on reload and via API GET /backfill/policies for that service. Given I enter an invalid deposit (negative or more than two decimal places), When I click Save, Then the form blocks submission and shows "Enter a valid non‑negative amount with up to 2 decimals" and no change is saved. Given a deposit is configured for service "Follow‑up", When a waitlist invite is generated for that service, Then the claim link requires payment of exactly the configured amount before confirming the slot. Given the deposit for a service is updated, When a new invite is generated after the update, Then it uses the new amount; previously sent invites retain their original amount and remain valid.
Enforce Eligible Time Windows and Lead Time
Given lead time is set to 90 minutes and the eligible session window is 08:00–20:00 local time, When a slot opens for 10:00 at 08:45, Then no invites are sent and the audit trail records "ineligible: lead time" with evaluated values. Given lead time is set to 60 minutes and current time is 10:50 with a 12:00 slot inside the eligible window, When the slot opens, Then invites are sent and the audit trail records the policy evaluation as "eligible". Given a slot starts at 07:30 and the eligible window starts at 08:00, When the slot opens, Then backfill is skipped and the audit trail records "ineligible: outside window" for the slot. Given an admin changes lead time from 60 to 30 minutes, When a subsequent slot opens, Then eligibility is evaluated against the new threshold and the policy change (old→new) is captured in the audit trail with actor identity and timestamp.
Prioritization Rules and Fair Rotation
Given prioritization is configured as "Least Recently Invited" with tiebreakers (then Waitlist Age ascending, then deterministic random seed), When ranking is computed for a slot, Then clients are ordered by lastInvitedAt ascending (null first), ties by joinedAt ascending, remaining ties by seeded random, and the full ordered list with reasons is recorded in the audit trail. Given an invite cap is configured as 2 invites per 7 days per client, When ranking is computed, Then clients at or above the cap are excluded from the current batch and the audit trail lists them as "ineligible: invite cap" with the window used. Given two clients A (lastInvitedAt=null, joinedAt t0+5d) and B (lastInvitedAt=t0−10d, joinedAt t0−20d), When ranking is computed, Then A ranks ahead of B and the audit entry shows the applied tiebreakers.
Batch Size, Channels, Templates, and Quiet Hours
Given batch size is set to 3 and quiet hours are 21:00–07:00 local, When a slot opens at 21:15, Then no invites are sent until 07:00 and the audit trail shows "deferred: quiet hours"; at 07:00 exactly 3 invites are sent. Given channel order is [SMS, Email] and both are enabled, When a client has SMS consent and a verified mobile, Then they receive an SMS using the configured SMS template; if SMS is not eligible, they receive an Email using the configured email template. Given templates include merge fields (client.firstName, slot.startLocal, service.name), When invites are sent, Then all merge fields render with correct values in the delivered messages. Given batch size is 3 and the waitlist has 10 eligible clients, When the first batch is sent, Then only 3 invites are dispatched and the remaining candidates remain queued (not contacted) with their pending status visible in the UI.
Manual Backfill Controls with Confirmations
Given an active backfill for a slot, When the admin clicks Pause and confirms in the modal (reason required), Then backfill status becomes Paused, pending/scheduled sends are halted, and an audit entry records actor, timestamp, and reason. Given a paused backfill, When the admin clicks Resume and confirms, Then backfill status becomes Active and future actions obey current policies; the action is logged in the audit trail. Given an invite to client X exists, When the admin clicks Skip and confirms with a required reason, Then X is removed from consideration for this slot, the ranking is recalculated, and the skip is recorded in the audit with the provided reason. Given a client Y is Banned via the action menu and the admin confirms scope (service or global), Then Y is excluded from future backfills per the selected scope, a banner appears on Y’s profile, and the ban action is logged with scope and actor identity. Given the admin selects Resend Invite for client Z and confirms, Then a new invite with a unique tracking ID is issued and the original invite is marked "Resent"; the resend event is recorded in the audit trail.
Immutable Audit Trail and Export for Compliance
Given any policy change, ranking, invite dispatch, invite click, payment event, confirmation, refund, or manual action occurs, When viewing the slot’s audit trail, Then a corresponding entry appears with eventType, ISO 8601 timestamp with timezone, actor (system or admin ID), and event payload. Given an audit entry exists, When attempting to edit or delete it via UI or API, Then the action is disallowed (UI has no edit/delete; API returns 405/403) and no changes occur to stored events. Given audit records exist, When exporting with filters (date range, slot ID, service ID, client ID, event types) to CSV and JSON, Then the files download successfully and contain only matching records with headers/keys, generatedAt, appliedFilters, and recordCount metadata. Given multiple audit entries for a slot, When fetching the audit list, Then entries are ordered chronologically by timestamp ascending with a deterministic secondary sort (sequence ID) and pagination returns consistent, non‑duplicated results across pages.
Auto‑Refund and Cancellation Policy Enforcement
Given a policy "Provider cancel = full refund" is enabled, When an admin cancels a backfilled appointment with a collected deposit, Then a full refund request is sent to the payment processor immediately, the client is notified via the configured template, and the refund event (amount, processor reference, policy name) is recorded in the audit trail. Given a policy "Client cancel ≥24h before start = full refund; 2–24h = 50% refund; <2h = no refund" is configured, When a client cancels at D hours before start, Then the refund amount matches the applicable bracket and the policy decision with evaluated D is logged in the audit. Given a claim payment fails or is declined, When the client taps Claim, Then the slot is not confirmed, the invite is marked Payment Failed, no refund is issued, and an audit entry records the failure reason from the processor. Given a refund request is created, When the payment processor reports the final outcome (succeeded or failed), Then the refund status is updated accordingly and reflected in the audit trail and UI with the processor reference.
Backfill Performance Analytics & Revenue Impact
"As a freelancer, I want clear analytics on how backfill performs so that I can optimize my settings and increase recovered revenue."
Description

Aggregate and present KPIs: time-to-fill, fill rate by lead time and service, conversion by channel/wave, deposit capture rate, no-show delta vs. non-backfilled sessions, incremental revenue recovered, and client-level engagement. Support cohorting (new vs. returning), export/CSV, and API access. Provide alerts for underperforming rules (e.g., low conversion from SMS at night) and A/B testing for batch sizes or prioritization weights. Outcome: practitioners see measurable value, enabling continuous optimization of policies and messaging to maximize utilization and cash flow.

Acceptance Criteria
KPI Coverage & Accuracy
- The analytics dashboard displays the following KPIs with the exact definitions and correct aggregation: time_to_fill_minutes, fill_rate_by_lead_time_and_service, conversion_rate_by_channel_and_wave, deposit_capture_rate, no_show_delta_vs_non_backfilled, incremental_revenue_recovered. - Given a validation dataset of at least 250 known backfill events, when metrics are computed, then KPI values match a reference calculation within ±0.5% for rates and exactly for counts and currency totals. - time_to_fill_minutes = floor((claim_deposit_timestamp - slot_open_timestamp) / 60s) and is reported per session and aggregated as median, p75, p90, and mean. - fill_rate_by_lead_time_and_service uses buckets [<1h, 1–6h, 6–24h, >24h] per service_id and is computed as filled_slots / open_slots within each bucket. - conversion_rate_by_channel_and_wave = claims / invites_sent grouped by invitation_channel and wave_number; multi-channel sequences attribute conversion to the last invite sent within 24h before claim. - deposit_capture_rate = deposits_captured / claims where deposit_status = "captured" within 15 minutes of claim. - no_show_delta_vs_non_backfilled = no_show_rate(backfilled) − no_show_rate(non_backfilled) matched by service_id and start_hour bucket, expressed in percentage points. - incremental_revenue_recovered = sum(deposit_amount + session_fee_collected − refund_amount) for backfilled sessions in the selected time range, reported in the provider’s currency.
Cohorting & Segmentation Controls
- Filters exist for client_cohort (new, returning), service_id, lead_time_bucket, channel, wave, and date_range; filters can be combined and applied to all KPIs and tables. - New vs returning is defined as: new = no completed sessions in the prior 365 days; returning = otherwise; the definition is consistently applied across all views. - For any filter combination, KPI numerators/denominators equal the sum of underlying filtered event rows within ±1 count; clearing filters restores global totals. - Filter state is preserved in shareable URLs and restored on page refresh; applying a saved view rehydrates the same results within ±0.5% of the original snapshot.
Client-Level Engagement Drilldown
- A client-level table is available that lists per client_id: invites_sent, opens, clicks, claims, deposit_captured (Y/N), avg_time_to_claim_minutes, last_engaged_at, lifetime_backfill_claims. - Clicking any KPI segment (e.g., channel = SMS, wave = 2) opens the drilldown scoped to the same filters; pagination (page size 50), column sorting, and search by client_id are supported. - Aggregation integrity: sum of claims in the drilldown equals the claims count on the parent KPI for the same filters within ±1. - Exporting the drilldown to CSV includes client_id and engagement metrics but excludes full PII (e.g., full email/phone), and the row count matches the on-screen total.
Data Export & API Access
- CSV export of the current view is available and includes columns sufficient to recompute all KPIs: slot_open_timestamp, claim_timestamp, invite_channel, wave_number, service_id, lead_time_bucket, client_cohort, deposit_status, deposit_amount, session_fee_collected, refund_amount, no_show_flag, session_id, client_id. - CSV export uses UTF-8 encoding, comma delimiter, RFC 4180 quoting, and renders timestamps in the provider’s configured timezone; exports up to 100k rows complete within 60 seconds. - API endpoints exist: GET /v1/analytics/backfill/kpis and GET /v1/analytics/backfill/events supporting filters (date_range, service_id, client_cohort, lead_time_bucket, channel, wave), cursor-based pagination (limit, cursor), API key auth, and 200 req/min rate limit. - API responses include a schema_version and totals that reconcile with the dashboard within ±0.5%; 429 responses are returned when rate limits are exceeded.
Underperforming Rule Alerts
- Users can configure alert rules with: metric, segment (e.g., channel=SMS, hour_bucket=21–06), threshold, minimum_sample_size, lookback_window, and notification channels (in-app, email). - When the observed metric stays below threshold and meets minimum_sample_size within the lookback_window, an alert is generated containing metric, current value, threshold, sample size, segment, and last_evaluated_at, plus a recommended action. - Alerts are de-duplicated per rule within a 24-hour window, support snooze options (7, 14, 30 days), and auto-resolve after two consecutive evaluations above threshold. - An immutable audit log records evaluations and alert state changes; exporting the log reproduces the counts and timestamps shown in the UI.
A/B Testing for Batch Size and Prioritization Weights
- Users can create experiments targeting batch_size or prioritization_weights with 2–4 variants; randomization occurs at the slot level with default equal allocation and optional custom weights. - Variant assignment is immutable per slot, timestamped, and emitted as an analytic event; live experiments allow only end-date changes, not treatment parameters. - The experiment report shows per-variant KPIs (conversion, time_to_fill, incremental_revenue, deposit_capture_rate, no_show_rate) and computes two-proportion z-tests for rates and t-tests for means; statistically significant differences are flagged at α=0.05. - Stopping an experiment freezes assignments and finalizes results; exporting per-variant events reproduces dashboard aggregates within ±0.5%.
Data Quality, Freshness, and Reconciliation
- Metrics refresh at least every 5 minutes; a visible "Last updated" timestamp is shown; if data is older than 15 minutes, a stale-data banner appears. - For any selected filters, KPI numerators and denominators reconcile to underlying event rows within ±1 count; discrepancies beyond this threshold trigger a warning badge. - Late-arriving events are incorporated on ingestion; affected KPIs recompute within one refresh cycle and an entry is added to a data-refresh log accessible to admins. - A daily synthetic dataset validation must pass 100% before publishing updates; failures block publishing and notify admins via email.

Fee Escalator

Enforces a clear, graduated fee schedule based on cancellation lead time (e.g., 0–2h = full fee, 2–24h = 50%). Pre‑auths up to the max fee at booking, then captures the correct amount automatically with a transparent breakdown on the invoice. Reduces awkward conversations and disputes by making outcomes predictable.

Requirements

Policy Tier Engine & Time-Window Calculation
"As a solo practitioner, I want cancellation fees to be automatically determined by clear time windows so that charges are consistent, predictable, and not manually calculated."
Description

Build a configurable rules engine that applies a graduated fee schedule based on cancellation lead time relative to the appointment start time. Supports global default policy and per-service and per-client overrides; multiple tiers (e.g., 0–2h full fee, 2–24h 50%, >24h $0); timezone-safe calculations (provider’s business timezone with client’s local timezone display); handles daylight savings, reschedules, and no-shows; determines applicable tier at the time of event (cancel, reschedule, no-show) and exposes the decision with human-readable labels and machine-readable codes for downstream billing and invoicing. Integrates with the scheduling module to receive event webhooks and with billing to pass the computed fee basis. Ensures immutability of historical policies via versioning and effective-dated rules to prevent retroactive changes.

Acceptance Criteria
Tier Determination by Lead Time (Provider Timezone Boundaries)
Given a policy with tiers defined as [>=0h and <2h => 100%], [>=2h and <24h => 50%], [>=24h => 0%] And a provider business timezone is configured When a cancellation event occurs 1h 59m before the appointment start (in provider timezone) Then the applied tier is 100% And the computed lead_time_minutes is 119 And the tier code and label correspond to the 0–2h window Given the same setup When a cancellation event occurs exactly 2h before start Then the applied tier is 50% And the computed lead_time_minutes is 120 Given the same setup When a cancellation event occurs exactly 24h before start Then the applied tier is 0% And the computed lead_time_minutes is 1440 Given the same setup When an event occurs after the scheduled start Then the applied tier is the lowest lead-time tier (100%) And lead_time_minutes is 0
Policy Scope Override Precedence (Client > Service > Global)
Given a global default policy G, a service-level policy S for Service A, and a client-level policy C for Client X When Client X cancels an appointment for Service A Then the engine applies policy C Given a global default policy G and a service-level policy S for Service A (no client override) When Client Y cancels an appointment for Service A Then the engine applies policy S Given only a global default policy G (no service or client overrides) When Client Z cancels any appointment Then the engine applies policy G Given conflicting effective versions across scopes at the event timestamp When the event is evaluated Then the engine selects the policy from the highest-precedence scope that is effective at the event timestamp And records the selected policy_scope and policy_version_id in the decision
Policy Versioning and Historical Immutability
Given policy version v1 is effective and a cancellation decision D1 is computed using v1 When an admin creates and activates policy version v2 after D1 Then D1 remains unchanged And the stored decision still references v1 And re-evaluating the event for audit returns the same tier as D1 Given v1 effective until T and v2 effective starting at T When an event occurs with timestamp < T Then v1 is used Given the same policies When an event occurs with timestamp >= T Then v2 is used Given a previously computed decision exists When the underlying policy definitions are edited or deleted Then historical decisions remain immutable and auditable with their original policy snapshots
Reschedule and No‑Show Event Handling
Given an appointment scheduled for a future time and a policy with time-based tiers When the appointment is rescheduled within a tiered penalty window (e.g., 90 minutes before start) Then the engine evaluates the reschedule event time against the original start time And applies the corresponding tier to the original appointment And emits a decision with event_type = RESCHEDULE and the correct tier code/label Given an appointment is rescheduled outside penalty windows (e.g., 36 hours before start) When the reschedule event is received Then the engine applies the $0 tier to the original appointment Given a no-show event is received at or after the scheduled start When the engine evaluates the event Then lead_time_minutes is 0 And the lowest lead-time tier is applied (e.g., full fee per policy) Given multiple reschedule or cancel events for the same appointment When subsequent events arrive after a decision has been finalized Then the engine maintains a single finalized decision for the original appointment And later duplicate events do not create additional charges
Timezone Safety and DST-Resilient Calculations
Given the provider business timezone is set and the client is in a different timezone When a cancellation occurs Then lead time is computed using the provider business timezone rules and absolute instants And the decision payload includes client_display_timezone and provider_timezone Given an appointment during a spring-forward DST transition day When a cancellation occurs that is exactly 2 real hours before start Then the engine classifies it in the same tier as 2 hours before start on a non-DST day Given an appointment during a fall-back DST transition day with an ambiguous local hour When a cancellation occurs at a timestamp representing exactly 2 real hours before start Then the engine classifies it consistently in the 2–24h tier Given any timezone or DST condition When a boundary time (exactly 2h or exactly 24h before start) is met Then the boundary is honored deterministically (lower bound inclusive, upper bound exclusive)
Decision Payload for Billing and Invoicing
Given a tiered decision is computed When the engine exposes the result to downstream systems Then the payload includes: tier_code, tier_label, policy_scope, policy_version_id, computed_lead_time_minutes, event_type, appointment_id, client_id, service_id, provider_timezone, client_display_timezone And the payload includes fee_basis_type (percent|flat), fee_basis_value, and base_amount_reference (e.g., session fee) And the payload is machine-readable (JSON) and human-readable label matches the policy definition Given the payload is received by billing When it contains a percent fee basis Then billing can compute fee_amount = round(base_amount * percent, policy rounding rules) Given the payload is received by invoicing When it contains a human-readable label Then the invoice line shows the label (e.g., "Late cancel (0–2h)") and the computed fee amount
Scheduling Webhook Integration and Idempotency
Given the scheduling module emits event webhooks for cancel, reschedule, and no-show with unique event_ids When the engine receives an event Then it evaluates the applicable policy/tier and persists a decision exactly once Given duplicate webhooks with the same event_id are received When the engine processes them Then the outcome is idempotent and only one decision record exists Given out-of-order events are received (e.g., cancel after a finalized reschedule decision) When the engine validates event ordering for the appointment Then older or superseded events are ignored for decisioning And an audit entry records the ignored event Given a successful decision is persisted When publishing to billing Then the engine includes a correlation_id matching the scheduling event_id
Booking-Time Pre-Authorization & Hold Management
"As a provider, I want a card pre-authorized when clients book so that I can reliably collect cancellation fees without chasing payments later."
Description

At booking, place a payment authorization up to the maximum potential fee defined by the policy, vaulting the payment method securely. Manage authorization hold lifecycles, including hold refresh or re-authorization if the appointment date is beyond card-network hold windows or after reschedules. Support SCA/3DS challenges where required, fallbacks for failed auths, and alternative flows (deposit/prepayment) when pre-auth cannot be obtained. Store client consent text and authorization details with audit trail. Expose clear UI states to both provider and client (authorized, expired, needs action) and send automated reminders when re-auth is needed. Conform to PCI and card network guidelines and prevent duplicate holds for back-to-back sessions.

Acceptance Criteria
Booking Pre-Auth and Vault at Checkout
Given a provider has an active Fee Escalator policy with a defined maximum potential fee for the booked service And the client provides a valid payment method When the client confirms booking Then the system requests a card-network authorization for the maximum potential fee And vaults the payment method as a token; no PAN or CVV is stored And persists authorization_id, authorized_amount, currency, auth_expiration_at, card_brand, last4 And marks the booking as Confirmed only if the authorization status is Authorized And returns a clear failure reason if authorization is Declined
Hold Refresh & Reschedule Re-Authorization
Given an existing active authorization with auth_expiration_at before the appointment end When time is 72 hours before auth_expiration_at Then the system attempts re-authorization for the same maximum potential fee using the vaulted token And, on success, updates authorization_id and auth_expiration_at and promptly voids the previous hold And, on failure, sets booking payment_state to Needs Action and triggers reminders And if the appointment is rescheduled beyond the current auth_expiration_at, the system immediately attempts re-authorization and voids the prior hold upon success And if rescheduled earlier within current hold validity, no new authorization is created
SCA/3DS Challenge Handling
Given the transaction requires SCA/3DS per network or issuer rules When the client confirms booking Then the system initiates a 3DS flow supporting frictionless and challenge modes And, if a challenge is required, presents the challenge to the client and awaits completion up to 10 minutes And, on successful authentication, completes the authorization and marks booking Confirmed And, on failure, timeout, or abandonment, marks payment_state as Needs Action, does not confirm booking, and provides a resumable link to complete SCA
Failed Pre-Auth Fallbacks (Deposit/Prepay)
Given a pre-authorization attempt is declined or SCA fails And the provider has a configured fallback policy (deposit percentage or fixed amount, or full prepayment) When the fallback flow is offered Then the client can complete the fallback payment successfully to confirm the booking And, on fallback payment success, payment_state is Confirmed with fallback_type recorded And, on fallback payment failure or no action within 24 hours, the booking is auto-canceled and both parties are notified with reason_code
UI States and Automated Re-Auth Reminders
Given any booking with a payment authorization lifecycle When the authorization status changes (Authorized, Re-Authorized, Expired, Needs Action, Failed) Then provider and client UIs reflect the new state within 60 seconds And each state includes a plain-language explanation and date/timestamps And when a re-authorization is required, the system sends reminders to the client at T-72h and T-24h with an actionable link And reminders are suppressed once re-authorization succeeds
Duplicate Hold Prevention for Back-to-Back Sessions
Given a client books multiple contiguous sessions with the same provider on the same day with gaps ≤ 10 minutes When placing holds for the block Then the system creates a single active authorization for the block_amount equal to the sum of each session’s maximum potential fee, capped by the provider’s configured daily hold cap And subsequent sessions added to the block update the block_amount via a single re-authorization; the previous hold is voided And canceling a session in the block reduces the block_amount via re-authorization or releases the hold if no sessions remain And at no point are two simultaneous active holds present for the same block
Consent, Authorization Details, and Audit Trail
Given a booking requiring payment authorization When the client reviews the payment terms Then the system displays the provider’s consent text and requires explicit acceptance (checkbox plus confirm) before attempting authorization And stores consent_text_version, accepted_at (UTC), client_id, ip_address, user_agent, and locale And persists an immutable audit record containing authorization_id, authorized_amount, currency, auth_expiration_at, network, card_brand, last4, 3DS_status/result, and actor/timestamps for create/update/void And sensitive authentication data (PAN, full track, CVV) is never stored or logged
Automatic Fee Capture on Cancel/No-Show/Reschedule
"As a provider, I want the correct fee to be captured automatically when a client cancels late or no-shows so that billing is accurate without manual intervention."
Description

When an appointment is canceled, marked no-show, or rescheduled inside a fee window, automatically compute the owed amount from the policy engine and capture funds against the existing authorization; if the hold is insufficient or expired, perform a new capture with stored payment method and retry on soft failures. Support partial capture, remaining hold release, tax calculation, multi-currency rounding, and itemizing adjustments. Ensure idempotency and guard against double charges by linking captures to appointment IDs. If the session occurs, convert the authorization into the session fee capture, net of any deposits, and release unused amounts. Emit events to the invoicing pipeline with line items and policy tier metadata.

Acceptance Criteria
Cancellation inside fee window with sufficient authorization
Given an appointment is canceled within a policy tier that charges a fee And a valid authorization hold exists with available amount ≥ owed fee When the cancellation is processed Then the owed amount is computed by the policy engine using the scheduled start time and cancellation timestamp And exactly the owed amount is captured against the existing authorization And any remaining unused hold is released within 5 minutes And the capture is linked to the appointment ID and policy tier code And an invoice event is emitted containing itemized line items (base fee, policy adjustment, tax) and policy tier metadata
No-show with expired authorization triggers new capture and retries
Given an appointment is marked no-show and the existing authorization is expired or voided When the no-show is processed Then the owed amount is computed by the policy engine And a new charge is attempted using the default stored payment method for the owed amount And on soft declines, the system retries up to 3 times over 24 hours with exponential backoff and stops on success or hard decline And on hard decline or final retry failure, the charge is marked failed and an invoice event with failure status is emitted And all charge attempts use the same idempotency key derived from the appointment ID to prevent duplicate charges
Reschedule inside fee window charges fee and sets new authorization
Given a client reschedules inside a fee window that incurs a reschedule fee And a valid authorization exists for the original appointment When the reschedule is processed Then the reschedule fee is computed by the policy engine and captured from the existing authorization And any remaining hold from the original appointment is released within 5 minutes And a new authorization hold is created for the new appointment’s maximum fee And an invoice event is emitted itemizing the reschedule fee with policy tier metadata and tax
Insufficient authorization amount triggers split capture
Given the owed amount exceeds the available amount on a valid authorization When the event (cancel/no-show/reschedule) is processed Then the system captures up to the available amount from the existing authorization And immediately charges the remaining balance using the stored payment method And any residual hold is released within 5 minutes And the sum of captured amounts equals the owed amount within currency rounding rules And a single invoice event is emitted with both capture references and itemized adjustments
Tax calculation and multi-currency rounding correctness
Given a tenant currency and tax configuration are set for the appointment’s location When the owed amount is computed and captured Then taxes are calculated per configuration on the taxable base and included as separate line items And monetary values are rounded to the currency’s minor unit using round half up And invoice subtotal + tax equals the total captured amount exactly in the appointment currency
Session occurs converts authorization to final fee net of deposit
Given the session is completed and a deposit was previously authorized or paid When invoicing the completed session Then the system captures from the existing authorization the session fee minus any deposit already captured And releases any unused authorization amount within 5 minutes And emits an invoice event with line items for session fee, deposit credit, and tax And ensures only one successful capture exists for the appointment ID
Idempotency under concurrent duplicate events
Given multiple concurrent or repeated cancel/no-show/reschedule signals are received for the same appointment When processing these signals Then at most one charge is executed for the appointment ID and event type And subsequent identical attempts return the existing charge reference and do not create new captures And only one invoice event with a stable event ID is emitted, with duplicates deduplicated downstream
Transparent Invoice & Client Receipt Breakdown
"As a client, I want to see exactly why I was charged a cancellation fee so that I understand the outcome and don’t feel surprised."
Description

Present an itemized invoice/receipt that clearly shows the standard session fee, the applied policy tier (e.g., “Late cancellation 50%”), the calculation basis (time window matched), and the final charged amount, including taxes and discounts. Include a link to the provider’s policy and timestamped consent. Provide client-facing explanations to reduce disputes and provider-facing internal notes (e.g., tier code, rule version). Support one-click insertion into existing SoloPilot session-to-invoice flows and export/PDF generation with consistent formatting.

Acceptance Criteria
Itemized Fee Escalator Breakdown on Invoice
Given a scheduled session has a defined standard fee and a matched fee escalator policy tier based on cancellation/no-show lead time When an invoice or client receipt is generated for the session Then the document displays the following fields: "Standard session fee", "Applied policy tier" (human-readable label), "Time window matched" (e.g., "2–24h"), "Calculation basis" (e.g., "50% of standard fee"), and "Final charged amount" And each monetary value is shown in the account currency and rounded to the account precision And the Final charged amount equals (standard_fee × tier_rate) And if no fee applies per the matched tier, the Final charged amount is 0 and the Applied policy tier clearly indicates "No charge"
Policy Link and Client Consent on Receipt
Given the client accepted the provider’s cancellation policy during booking and the policy snapshot was stored When the client-facing invoice or receipt is generated Then it shows a clickable link to the exact policy snapshot and the client’s consent timestamp in the account timezone And the link opens the policy in a new tab/window And the consent timestamp matches the booking record and is immutable
Taxes and Discounts Order of Operations After Fee Adjustment
Given taxes and/or discounts are configured for the account And a fee escalator tier applies to the session When the invoice is generated Then the calculation order is: base = (standard_fee × tier_rate); net = (base − eligible_discounts); tax = tax_rate applied to net; total = (net + tax) And the invoice itemization shows separate lines for discounts and taxes with labels, rates, and amounts And subtotal, tax, and total values match the above order within rounding rules (≤ 1 minor currency unit)
Client vs Provider Visibility Controls
Given there are client-facing and provider-facing views of the invoice/receipt When the document is rendered Then the client-facing view includes: standard fee, applied policy tier label, time window matched, calculation basis, final amount, taxes/discounts, policy link, and a brief client-friendly explanation And the client-facing view hides provider-only internal notes (e.g., tier code, rule version, rule ID, evaluation trace) And the provider-facing view includes provider-only internal notes: tier code, rule version, rule ID, policy evaluation timestamp, and evaluation trace/reference ID
Pre-Authorization and Capture Representation
Given the system pre-authorizes up to the maximum fee at booking And the final charge is determined by the matched fee escalator tier When payment is captured Then the captured amount equals the Final charged amount shown on the invoice And any unused pre-authorization amount is released by the processor and the provider-facing notes include pre-auth reference, original pre-auth amount, captured amount, and release status And if capture fails, the invoice reflects a failed payment status and no client receipt is issued until a successful capture occurs
One-Click Session-to-Invoice Insertion
Given a provider uses the SoloPilot one-click session-to-invoice flow When converting an eligible session into an invoice Then the invoice auto-includes the fee escalator breakdown block with all required fields populated from the session and policy evaluation And the provider can remove or re-insert this block with one click prior to sending And the block persists across save, send, export, and PDF actions, retaining linkage to the session ID
PDF/Export Formatting Consistency
Given an invoice/receipt containing a fee escalator breakdown is exported or printed to PDF When the export/PDF is generated Then the exported document preserves itemization order and labels, currency formats, tax/discount lines, policy link (as a clickable URL), consent timestamp, and totals And page layout keeps the breakdown with the primary line item unless content overflow requires pagination, in which case section headers repeat on the next page And all numeric values and totals in the export match the on-screen values exactly
Policy Communication, Disclosure, and Consent Capture
"As a provider, I want clients to acknowledge the cancellation policy at booking so that fees are enforceable and expectations are clear."
Description

Display a concise summary of the cancellation fee policy during booking on all channels (web booking page, manually created appointments, mobile), require explicit checkbox consent, and include the policy in confirmation/reminder messages with the relevant time windows. Store signed consent text, timestamp, IP/device metadata, and policy version for auditability. Allow providers to customize copy and locale, and ensure accessibility and plain-language readability. Sync consent artifacts to the client’s profile and appointment record.

Acceptance Criteria
Web Booking: Policy Summary and Consent Required
Given a client selects an appointment time on the web booking page, when the booking form is shown, then a concise cancellation policy summary is displayed above the booking action including fee windows (e.g., 0–2h full fee, 2–24h 50%) and notice of pre-authorization up to the maximum fee. Given the consent checkbox is unchecked, when the client attempts to book, then the Book button remains disabled and an accessible error text indicates consent is required. Given the consent checkbox is checked, when the client books, then the system captures and stores the exact consent text shown, policy version, UTC timestamp, client IP, device/user agent, and locale, and links it to the client and appointment records. Given the booking completes, then the appointment record displays the consent version and a link to the stored artifact.
Manual Appointment Creation: Consent Capture Workflow
Given a provider manually creates an appointment in the admin, when the client has no consent matching the current policy version, then the system blocks saving and prompts to send a consent request or record verbal consent with client affirmation and timestamp. Given a consent request is sent, when the client completes the consent, then the appointment status updates from Pending Consent to Scheduled and the consent artifact is linked to the appointment. Given the client has an existing consent matching the current policy version, when the provider saves the appointment, then no new consent is required and the existing consent is linked.
Mobile Booking: Responsive Display and Consent
Given a client books via mobile web or app, when the booking screen is displayed, then the policy summary is visible without horizontal scrolling and supports expanding More content for full details. Given the consent checkbox on mobile, when it is unchecked, then booking is disabled; when checked, booking is enabled. Given poor connectivity during submission, when the client taps Book, then the system retries consent capture and booking up to 3 times with user feedback; if all retries fail, no appointment is created and an error advises retry. Given booking succeeds, then stored consent includes mobile device metadata (user agent/platform) and client IP.
Confirmation and Reminder Messages: Policy Inclusion with Time Windows
Given an appointment is scheduled, when a confirmation email or SMS is generated, then it includes the cancellation policy summary with fee windows calculated from the appointment start and a note about pre-authorization up to the maximum fee. Given a reminder is generated within a specific fee window, then that window is clearly indicated (e.g., within 2 hours: full fee applies). Given locale settings, when messages are generated, then dates, times, currency, and text are formatted per the client’s locale and provider customization. Given messages are sent, then a copy or render reference is stored with the appointment for audit.
Consent Artifact Storage, Versioning, and Auditability
Given consent is captured on any channel, then the system stores: exact consent text presented, policy version ID, UTC timestamp, client ID, appointment ID (if applicable), IP address, device or user agent, locale, channel, and initiator (client or provider). Given audit export is requested for a client or appointment, when performed, then all related consent records are returned in chronological order with immutable IDs and content hashes. Given the policy version changes, when a client books after the change, then a new consent for the new version is required; historical records remain unchanged and linked to prior appointments.
Customization, Localization, Accessibility, and Readability
Given a provider updates policy copy and selects available locales in settings, when saved, then booking UIs and messages immediately reflect the customized text and locale without code changes. Given a client’s language preference or browser locale matches a configured locale, then the corresponding policy copy is served; otherwise the provider default is used. Given accessibility testing, then the policy and consent UI meets WCAG 2.1 AA for keyboard navigation, focus visibility, color contrast of at least 4.5:1, and screen reader labels for all controls. Given readability analysis runs at save time, then the policy copy scores at or below an 8th-grade reading level in English (or equivalent), or a warning is displayed if higher; saving remains allowed with warning.
Sync to Client Profile and Appointment Record
Given consent is captured, when viewing the client profile, then a Consents section lists each record with date and time, policy version, channel, and a link to the stored artifact. Given an appointment is viewed, then the consent used for that appointment is shown; if multiple consents exist, the one effective at booking time is displayed. Given API consumers request client or appointment details, then consent metadata is available via API fields: consentVersion, consentTimestamp, consentIp, consentDevice, and consentTextRef.
Admin Overrides, Waivers, and Permissions
"As a provider, I want the ability to waive or adjust a cancellation fee with a clear record so that I can handle special cases without breaking my books."
Description

Enable authorized users to override policy results per appointment: waive or reduce fees, change the applied tier, or issue credits; require reason codes and add internal notes. Enforce role-based permissions and record a tamper-evident audit trail capturing user, timestamp, before/after values, and client notification status. On override, regenerate invoices/receipts and update ledger entries. Provide quick actions from the schedule view and the invoice screen.

Acceptance Criteria
Override Full Fee to Waiver from Schedule View
Given an appointment has an auto-applied full-fee cancellation charge and an unpaid invoice version v1 exists And the signed-in user has permission override.waive When the user clicks Quick Action "Override" on the schedule card, selects "Waive 100%", chooses a required reason code, and enters an internal note of at least 10 characters Then the system sets the appointment’s applied tier to "Waived" and updates the fee to $0.00 And regenerates the invoice to version v2 with a visible "Waived" line item and reason code reference And posts ledger entries that reverse the prior charge and net to $0.00 for the appointment And writes an immutable audit entry capturing user, timestamp, before/after tier and amounts, reason code, internal note, and client notification status = "Not Sent" And updates the schedule card badge to "Waived" within 30 seconds
Reduce Fee or Issue Credit from Invoice Screen
Given an invoice for a canceled appointment is open in the invoice screen with auto-calculated fee and tax And the signed-in user has permission override.reduce and/or override.credit When the user selects "Adjust Fee" and enters either a percentage or currency reduction with a required reason code and note Then the invoice total recalculates including taxes/discounts, and a new version is created And if payment was not yet captured, the pending capture amount is updated to the new total and excess authorization is released And if payment was already captured, a partial refund for the difference is issued and recorded And the ledger records an adjustment entry linked to the invoice version and appointment When the user selects "Issue Account Credit" instead of reducing the fee Then a client credit memo is created for the specified amount, linked to the appointment and invoice, without changing the applied tier And the invoice shows the credit application details, and the client account balance increases accordingly And an audit entry records the action, reason code, note, before/after invoice totals, and payment/refund identifiers
Change Applied Cancellation Tier with Reason Code
Given an appointment has an auto-selected cancellation tier based on lead time And the signed-in user has permission override.changeTier When the user selects "Change Tier" and chooses a different tier from the configured schedule and provides a required reason code and note Then the system recalculates the fee per the selected tier And regenerates the invoice to a new version reflecting the new tier and amount And posts corresponding ledger adjustments (reversal and new charge) linked to the appointment And records an audit entry with before/after tier, amounts, user, timestamp, reason code, internal note, and client notification status
Role-Based Permissions Enforcement
Given roles/permissions are configured for override.waive, override.reduce, override.changeTier, and override.credit When a user without the required permission views the schedule card or invoice screen Then override quick actions are not visible or are disabled with a tooltip explaining required permission When an unauthorized user attempts to call an override API directly Then the request is rejected with HTTP 403 and no financial or invoice changes occur And a security audit entry records the denied attempt with user, timestamp, and endpoint When an authorized user performs an override Then the action succeeds and is logged with the acting user’s identity; system-level accounts cannot be used to mask actor identity
Tamper-Evident Audit Trail and Immutability
Given one or more overrides/credits have been applied to an appointment or invoice When viewing the audit trail for that appointment/invoice Then each entry displays user, timestamp (UTC with timezone offset), action type, reason code, internal note, before/after values (tier, amounts), and client notification status (Sent/Not Sent/Failed with channel) And entries are immutable in UI and API; edit/delete endpoints are unavailable and attempts return HTTP 405 And each entry includes a sequential ID and integrity hash; the audit verification endpoint returns "Pass" for the sequence And exporting the audit log reproduces the same hashes for verification
Invoice Regeneration and Ledger Consistency
Given an existing invoice vN is associated with an appointment When any override/credit/tier change is saved Then a new invoice version vN+1 is created with a unique version number, updated PDF/receipt, and visible change summary And prior invoice versions become read-only but remain accessible for audit And ledger entries are posted as linked reversals and new postings that reconcile to the new invoice total And the GL export includes both the reversal and new postings with matching reference IDs And the invoice and ledger totals match exactly to the cent across UI, API, and exports
Client Notification Options and Logging
Given an override has been applied and the invoice regenerated When the user chooses "Notify Client" and selects Email or SMS and a message template Then the client receives the updated invoice/receipt link, and delivery status is recorded And the audit entry updates client notification status to "Sent" with channel, timestamp, and message ID When delivery fails Then status is "Failed" with error code and a retry option is available; retries are also logged When the user explicitly selects "Do Not Notify" Then audit status remains "Not Sent" and the client portal still reflects the updated balance immediately
Cancellation Insights & Recovery Reporting
"As a business owner, I want to see how much revenue the fee policy recovers and where it fails so that I can tune my policy and communications."
Description

Provide dashboards and exports showing cancellation counts by tier, recovered revenue, override rates, pre-authorization failure rates, dispute outcomes, and average lead time to cancellation. Support filtering by date range, service, client, and location, with trendlines and cohort comparisons. Surface actionable alerts (e.g., unusually high late cancellations) and suggestions (e.g., adjust tiers). Integrate with existing reporting and allow CSV export and API access.

Acceptance Criteria
Insights Dashboard Metrics Coverage
- Given the reporting module, When the user opens Cancellation Insights, Then the dashboard displays cards for: Cancellation count by fee tier, Recovered revenue, Override rate, Pre-authorization failure rate, Dispute outcomes (win/loss), and Average lead time to cancellation. - Given a known test dataset, When the dashboard loads, Then each metric matches the backend aggregation spec within 0.1% or ±1 count (whichever is greater). - Given a new cancellation is recorded, When the dashboard is refreshed, Then metrics reflect the event within 15 minutes. - Given any metric card, When the user clicks "View details", Then a drilldown table opens filtered to the contributing records.
Filter and Segmentation Controls
- Given the Cancellation Insights view, When filters are applied for date range, service (multi-select), client (multi-select), and location (multi-select), Then all widgets and tables update to reflect the intersection (AND) of selected filters. - Given no filters are set, When the page loads, Then the default date range is Last 30 Days and all services, clients, and locations are included. - Given filters are set, When the URL is copied and reopened, Then the exact filter state is restored and shareable. - Given up to 50,000 relevant events in range, When a filter is changed, Then all visuals update within 2 seconds at p95.
Trendlines and Cohort Comparisons
- Given the trend view, When a user selects two or more cohorts, Then the line chart plots all series with a clear legend and distinct colors. - Given the cohort picker, When the user selects a dimension of Service, Client, or Location, Then the available values reflect current filters and up to 5 cohorts can be compared simultaneously. - Given a date range ≥ 2 weeks, When "Compare to prior period" is toggled, Then a comparison series renders with percentage change and absolute delta for each point. - Given a chart point, When the user hovers, Then a tooltip displays the metric value, cohort label, and delta vs comparison for that timestamp. - Given a long date range, When the user changes date grain, Then the chart supports daily, weekly, and monthly aggregation with correct rollups.
Actionable Alerts and Suggestions
- Given historical data, When the late-cancellation rate (0–2h tier) exceeds 20% for any service over the last 14 days with at least 30 cancellations, Then an in-app alert labeled "High late cancellations" is created for that service. - Given an alert exists, When the user opens it, Then the detail view shows threshold, observed value, sample size, 14-day trendline, and a link to affected appointments. - Given patterns indicating threshold gaming (e.g., spikes within ±2h of a tier boundary) with ≥30 events and ≥5% increase, When detected, Then a suggestion is surfaced to adjust the tier boundary with an estimated revenue impact. - Given notification settings are enabled, When a new alert is created, Then Admins receive an email and in-app notification; non-Admins do not. - Given an alert is acknowledged, When the same condition persists, Then duplicate notifications are suppressed for 14 days while the alert remains visible with status "Acknowledged". - Given org settings, When an Admin edits alert thresholds, Then changes take effect within 10 minutes and are audit-logged.
CSV Export for Cancellation Insights
- Given applied filters, When the user clicks "Export CSV", Then a CSV is generated at the event level with columns: appointment_id, client_id, service_id, location_id, scheduled_start_at, cancellation_at, lead_time_minutes, fee_tier, preauth_status, captured_amount, recovered_flag, override_flag, dispute_status, organization_tz. - Given the export completes, When downloaded, Then timestamps are ISO 8601 with timezone offset, numeric fields use dot decimal, encoding is UTF-8, line endings are LF, and values reconcile to on-screen totals within 0.1% or ±1 count. - Given an export >100,000 rows, When requested, Then it runs as a background job and a secure download link is delivered via email within 10 minutes. - Given a user without export permission, When they attempt to export, Then the action is disabled with a tooltip "Insufficient permissions" and no job is created.
API Access to Cancellation Insights
- Given a valid API token with reporting.read scope, When calling GET /api/v1/reports/cancellations with filters (date_range, service_ids[], client_ids[], location_ids[]), Then the response includes metrics (by fee_tier counts, recovered_revenue, override_rate, preauth_failure_rate, dispute_outcomes, avg_lead_time_minutes) and a paginated raw events endpoint at /api/v1/reports/cancellations/events. - Given pagination parameters page and page_size ≤ 1000, When requesting events, Then results are ordered by cancellation_at desc with next/prev cursors and stable ordering across pages. - Given normal load, When a request is made, Then p95 latency ≤ 1000 ms and rate limiting is enforced at 60 requests/min per token with 429 responses on exceed. - Given versioning, When the Accept header is application/vnd.solopilot.v1+json, Then the response adheres to v1 schema and includes a Deprecation header when applicable. - Given an invalid filter, When the request is made, Then the API returns HTTP 400 with a descriptive error including the offending parameter name and allowed values.
Integration and Permissions within Reporting
- Given existing reporting navigation, When the feature flag fee_escalator_reporting is enabled, Then "Cancellation Insights" appears under Reports and respects role-based access: Admin and Manager have read access; Staff has no access by default. - Given a user with access, When opening a metric card or table row, Then the user can drill down to the underlying appointment, invoice, and dispute records in new tabs. - Given SSO is enforced for the organization, When a deep link to a filtered view is opened, Then access is gated by SSO and the saved filters are applied after authentication. - Given audit logging is enabled, When a user exports data or views API credentials, Then an audit event is recorded including user_id, action, timestamp, and filter summary.

Waiver Ledger

Extends one‑tap waivers with a reason picker, private notes, and optional client flags (e.g., first‑time courtesy). Caps how often waivers can be used per client, tracks recovered vs. forgiven revenue, and shows retention impact—so you can be compassionate without opening the floodgates.

Requirements

Reason Picker on Waiver Action
"As a solo practitioner, I want to choose a standardized reason when I waive a fee so that I can analyze patterns and enforce consistent policies."
Description

Add a mandatory, admin-configurable reason selector to the one-tap waiver flow across invoices, session records, and payments. Support hierarchical reasons (category → subreason), optional free-text addendum, localization, and API-safe enum values. Persist the selected reason to a dedicated waiver ledger record, surface it in internal history and reporting, and keep it hidden from client-facing documents by default (admin-toggleable). Provide validation, error states, sensible defaults per workflow, and graceful handling of legacy waivers (mapped to “Unspecified”). Enable search and filters by reason in the ledger and reporting views.

Acceptance Criteria
Mandatory Reason Picker Across Waiver Entry Points
Given Waiver Ledger is enabled and at least one reason category (with optional subreasons) exists When a staff user initiates a one-tap waiver from an invoice, session record, or payment Then a reason picker modal is presented before the waiver can be confirmed And the Confirm/Apply action remains disabled until all required selections are made (category and subreason if required) And if a workflow default reason is configured, it is pre-selected but requires explicit user confirmation before apply And attempting to submit with missing required fields shows inline validation messages and focuses the first invalid field And if a network or save error occurs on apply, the user sees a non-dismissive error banner and can retry without losing selections
Admin-Configurable Hierarchical Reasons
Given an admin with permissions opens Waiver Reasons settings When the admin creates categories and subreasons and sets Requires Subreason on specific categories Then the picker displays categories and only shows subreasons for the selected category And only active categories/subreasons are selectable; inactive ones are hidden from the picker And reasons with usage cannot be hard-deleted; they can only be deactivated and remain visible on historical records And reordering categories/subreasons updates display order without altering historical records And changes take effect immediately for new waivers without requiring a page reload
Optional Free-Text Addendum
Given the reason picker is open When the selected reason is flagged as Requires Addendum or the subreason is Other Then an Addendum text area becomes required and enforces a max length of 500 characters And if the reason does not require an addendum, the Addendum field remains optional and accept 0–500 characters And submitting with an over-limit addendum blocks apply and shows a remaining-character counter and error state And the addendum value is saved with the waiver record and is not shown on client-facing documents by default
Localization and Fallback Behavior
Given the workspace default locale and user display locale are configured When the reason picker renders Then category and subreason labels appear in the user’s locale if provided by the admin And if a label is missing for the user’s locale, it falls back to the workspace default, then to the enum key as a last resort And switching the user locale updates displayed labels without changing stored enum values And exports and reports include both localized labels (for the chosen export locale) and enum keys
API-Safe Enum Values and Backward Compatibility
Given each category and subreason has an immutable API-safe enum key When a waiver is created via API Then the request must include reasonEnum and optional subreasonEnum that match configured enums And the API rejects unknown or inactive enums with HTTP 400 and a machine-readable error code And responses include reasonEnum and subreasonEnum along with localized labels when a locale is specified And legacy waivers created before this feature are returned with reasonEnum=UNSPECIFIED and no subreasonEnum And deactivating a reason does not change existing enums on historical records or break API reads
Persistence, Internal Visibility, and Client-Facing Privacy Toggle
Given a waiver is applied When the record is saved to the Waiver Ledger Then it stores reasonEnum, subreasonEnum (if any), addendum, actorUserId, sourceType (invoice/session/payment), sourceId, timestamp, and locale at time of action And internal history and reporting views display the reason and addendum to authorized staff And client-facing documents (invoices, receipts, statements) do not show reason or addendum by default And if an admin enables the setting “Show waiver reason on client documents,” only the category label is displayed on newly generated documents; addendum is never shown And previously issued client documents are not retroactively altered
Search and Filter by Reason in Ledger and Reporting
Given the Waiver Ledger and reporting views are open When the user filters by reason category and/or subreason (multi-select supported) Then results update to include only matching records, including a selectable Unspecified group for legacy waivers And search supports both localized labels and enum keys And aggregate metrics (counts, totals) recalculate to reflect the filtered set And exporting from a filtered view includes only filtered records with reason enums and labels And filter and result updates complete within 2 seconds for datasets up to 50k waiver records
Private Waiver Notes
"As an account owner, I want to add private context to a waived charge so that my future self understands the decision without exposing it to the client."
Description

Attach internal-only notes to each waiver entry with role-based visibility and retention controls. Support plain text with lightweight formatting, mentions, and a character limit; exclude from all client-facing artifacts (invoices, emails, portals). Link notes to the client profile and the originating invoice/session. Notes are editable within a short grace period and then locked, with admin override requiring justification; all changes are recorded in the audit trail. Enable search within notes for authorized users and include notes in data export for compliance.

Acceptance Criteria
Create Internal Waiver Note with Formatting and Character Limit
Given a waiver entry exists and the user has permission (Owner/Admin/Billing/Practitioner), When the user opens Add Note and enters text using bold, italic, bullet lists, and @mentions, Then the preview renders supported formatting and @mentions resolve to selectable team members. Given the user types more than 2000 Unicode characters, When they attempt to save, Then the save is blocked, an error message is shown, and the counter shows 2000/2000. Given a note is saved with allowed formatting, When retrieved via UI or API, Then the stored content is sanitized (no HTML/scripts) and preserves only supported lightweight formatting and mentions. Given a user without create permission attempts to add a note, When saving, Then the action is denied with 403/permission error.
Role-Based Visibility and Client Exclusion
Given a waiver note exists, When viewed by Owner/Admin/Billing/Practitioner, Then the note content and metadata are visible. Given the same note, When viewed by Client/Guest/External collaborators without waiver-note permission, Then the note is not rendered, endpoints do not return it, and no metadata leaks (e.g., counts/placeholders). Given an invoice PDF or email is generated, When delivered to the client, Then the waiver note content and any references are excluded. Given a client signs into the portal, When navigating waivers, invoices, or sessions, Then the waiver note is never displayed or hinted (no UI elements).
Grace-Period Editing and Locking with Admin Override
Given a waiver note was created less than 15 minutes ago by its author, When the author edits and saves, Then the note updates successfully and an audit entry is recorded. Given a waiver note is older than 15 minutes, When any non-admin attempts to edit, Then the edit control is disabled and save is blocked. Given a waiver note is older than 15 minutes, When an Admin initiates an override edit and provides a non-empty justification, Then the edit is permitted, the justification is stored, and the note re-locks after save. Given an Admin attempts an override edit without justification, When saving, Then the action is blocked with an error. Given client and device times may differ, When evaluating the 15-minute window, Then server time is used as the source of truth.
Audit Trail for Waiver Note Lifecycle
Given create, edit (within grace), lock, override edit, retention change, or purge occurs for a waiver note, When querying the audit log, Then each event appears with UTC timestamp, actor ID, action type, waiver note ID, and reason (if applicable). Given an edit occurs, When viewing the audit details, Then the prior content version is retrievable and a diff against the new version is shown. Given audit logs exist, When a non-authorized user (non-Owner/Admin) attempts to view them, Then access is denied. Given audit entries are written, When attempting to modify or delete an audit record via UI or API, Then the operation is rejected (audit entries are immutable).
Linking Notes to Client and Originating Invoice/Session
Given a waiver note is created from an invoice or session, When saved, Then the note stores references to client ID and the originating invoice/session ID and displays navigable links. Given the originating invoice is archived or the session is canceled, When viewing the note, Then the links remain valid and open historical records. Given a client profile is opened by an authorized user, When viewing the Waiver Notes section, Then the note appears with timestamp and links back to the waiver entry and originating invoice/session.
Search Within Waiver Notes (Authorized Users)
Given multiple waiver notes exist, When an authorized user searches by keyword, Then matching notes are returned ranked by relevance/date within 2 seconds for up to 10,000 notes. Given a query contains an @mention or an exact phrase in quotes, When executed, Then results include notes that mention the specified user or contain the exact phrase. Given an unauthorized user executes a search, When results load, Then no waiver notes are returned and no counts/snippets are leaked. Given search results are opened, When viewing a note, Then only content allowed by the user's role is displayed.
Retention Policy and Compliance Data Export
Given a workspace retention policy of 7 years is configured for waiver notes, When a note exceeds 7 years and is not on legal hold, Then the note is purged from UI, search index, and exports, and a purge audit entry is recorded. Given a legal hold is applied to a client or specific note, When the retention window elapses, Then the note is retained and remains discoverable to authorized users. Given an Admin initiates a compliance export by client or date range, When the export completes, Then the file includes waiver notes with fields: note ID, waiver ID, client ID, invoice/session ID, author ID, created/updated timestamps, locked status, content, and audit reference. Given a purged note existed, When a new export is generated, Then the purged note content is excluded while its purge audit entry is present in the audit export. Given a non-authorized role attempts to export, When initiated, Then the operation is denied. Given an export is downloaded, When opened, Then it is UTF-8 encoded and conforms to the documented schema.
Client Courtesy Flags & Usage Caps
"As a coach, I want first-time courtesy limits per client so that I can be generous once without creating ongoing expectations."
Description

Introduce client-level flags (e.g., First-time Courtesy, Hardship) and enforce configurable waiver usage limits. Support per-client caps by count and monetary amount over rolling or calendar windows, with service-type exceptions. At waiver time, display remaining allowance, warn near limits, and block or require elevated approval when limits are exceeded. Allow authorized overrides with mandatory justification. Show flags and cap status on the client profile and in the waiver modal; support bulk import/backfill and automated flag assignment via rules.

Acceptance Criteria
Client Flags and Cap Status Visibility
Given a client has the flags "First-time Courtesy" and "Hardship", When a user views the client profile, Then both flags and current cap status (remaining count, remaining amount, window type) are visible. Given a user lacks the "Manage Client Flags" permission, When they attempt to add or remove a flag, Then the action is disabled or denied with a permission message. Given a user has the "Manage Client Flags" permission, When they add or remove a flag, Then the change is saved, reflected immediately on the profile and waiver modal, and an audit entry is recorded with timestamp and actor. Given a client, When the waiver modal is opened, Then the same flags and cap status are displayed at the top of the modal.
Rolling and Calendar Window Cap Enforcement
Given caps are configured as count=2 per 90-day rolling window and amount=$200 per calendar month, And the client has already used 2 waivers in the last 90 days, When a new waiver is submitted, Then the system evaluates the count cap as exceeded for this client. Given the oldest waiver falls outside the 90-day window on day 91, When a new waiver is submitted on day 91, Then the count cap is considered available for this client. Given the client has $180 waived in January, When a $40 waiver is submitted on January 31, Then the amount cap for January is evaluated as exceeded for this client. Given it is February 1 and the amount cap resets monthly, When a waiver is submitted, Then the amount cap usage is reset for February for this client.
Waiver Modal Allowance Display and Near-Limit Warning
Given caps are configured and usage is up to date, When the waiver modal is opened, Then it shows remaining count and remaining amount with window labels (e.g., "90-day rolling", "This month"). Given remaining allowance is at or below 20% of the configured cap, When the waiver modal is opened, Then a non-blocking near-limit warning banner is shown. Given a cap is exceeded, When the waiver modal is opened, Then a clear exceeded-cap banner is shown and the primary action reflects the configured policy (blocked or approval required).
Exceeding Cap Behavior: Block vs. Approval Workflow
Given organization policy is set to Block when exceeded, And the client’s waiver would exceed either the count or amount cap, When the user submits the waiver, Then submission is blocked with a message specifying which cap is exceeded and by how much. Given organization policy is set to Require approval when exceeded, And the client’s waiver would exceed either cap, When the user submits the waiver, Then an approval request is created, the waiver enters Pending state, and the requester is notified. Given an approval request exists, When an approver reviews it, Then they can Approve to apply the waiver or Reject to cancel it, and the requester is notified of the outcome.
Authorized Override with Mandatory Justification and Audit
Given the client exceeds a cap and the user has the "Waiver Override" permission, When the user selects Override, Then a justification field is mandatory and must contain at least 10 characters to enable submission. Given a valid justification is provided, When the override is submitted, Then the waiver is applied, usage is updated accordingly, and an audit record is captured including user, timestamp, justification, cap state, and service type. Given a user lacks the "Waiver Override" permission, When they attempt to override, Then the option is unavailable or the action is denied with a permission message.
Service-Type Exceptions to Caps
Given service type "Initial Consultation" is configured as exempt from caps, When a waiver is applied to an appointment of that service type, Then the waiver is allowed and does not consume count or amount caps. Given a service type is not exempt, When a waiver is applied, Then the waiver consumes caps according to configured rules. Given a service type exemption applies, When the waiver modal is opened for that appointment, Then the UI indicates the exemption and no cap warning or block is shown due to caps.
Bulk Import/Backfill and Rule-Based Flag Assignment
Given a CSV with columns client_id, flags, cap_count_used, cap_amount_used, window_reference_date, When the file is imported by an authorized admin, Then valid rows update clients and invalid rows are rejected with row-level error messages. Given the same import file is re-run, When processed, Then the operation is idempotent with no duplicate flags or double-counted usage. Given an automation rule "On first waiver ever, add First-time Courtesy" is enabled, When a client’s first waiver is recorded, Then the "First-time Courtesy" flag is automatically assigned and visible in the profile and waiver modal.
Recovered vs. Forgiven Revenue Tracking
"As a therapist, I want to see how much waived revenue I later recoup so that I understand the real cost of waivers."
Description

Extend the ledger to distinguish forgiven (written off) versus recoverable waivers and support partial and full recoveries. Link recoveries to subsequent payments or invoice line items, automatically updating recovered/forgiven metrics and remaining balances. Provide actions to reverse or convert waiver types with proper accounting integrity and audit entries. Expose totals and trends in reporting and on the client profile, and ensure exports reflect waiver type, amount, and recovery references.

Acceptance Criteria
Partial recovery allocation to recoverable waiver
Given a client has an invoice with a $200 recoverable waiver applied to line item A and the waiver shows recovered=$0, forgiven=$0, remaining=$200 When a $75 payment is posted and explicitly linked as a recovery to invoice line item A Then the waiver updates to recovered=$75 and remaining=$125 within 5 seconds And the waiver ledger records a recovery entry with paymentId, invoiceId, lineItemId, timestamp, and actorId And reporting and the client profile reflect +$75 recovered revenue for the payment date’s reporting period And the system prevents recovery allocation that exceeds the waiver’s remaining amount
Full recovery and balance zeroing
Given a recoverable waiver with remaining=$125 When a $125 recovery is linked via payment allocation to the waived line item Then the waiver updates to recovered increased by $125 and remaining=$0 within 5 seconds And no additional recoveries can be applied to this waiver unless the waiver is modified or reversed And the ledger records a completed recovery entry and marks the waiver as fully recovered And reporting shows no over-recovery (cap at waived amount)
Forgiven waiver write-off behavior
Given a waiver is created with type=Forgiven and amount=$180 When the waiver is saved Then the waiver records forgiven=$180, recovered=$0, remaining=$0 And the waiver cannot be linked to any future payments or invoice line items unless converted to Recoverable And the client profile and reporting reflect +$180 forgiven revenue for the waiver date’s reporting period And the ledger creates an audit entry capturing type=Forgiven, amount, reason, and actorId
Convert waiver type with audit and recalculation
Given a recoverable waiver with amount=$200 and recovered=$50 (remaining=$150) When the user converts the waiver type to Forgiven Then the system freezes recovered=$50 and sets forgiven=$150, remaining=$0 And an audit entry records the conversion (from Recoverable to Forgiven) with previous and new state, timestamp, and actorId And reporting reclassifies $150 from recoverable-remaining to forgiven totals without duplicating the $50 already recovered And exports include a conversion flag and reference to the originating waiver id Given a forgiven waiver with no recoveries When the user converts the waiver to Recoverable Then remaining equals the full forgiven amount and the waiver becomes eligible for future recoveries with audit recorded
Reverse recovery and handle payment refund
Given a waiver with recovered=$120 linked to payment P1 and remaining=$80 When the user reverses the recovery link to P1 Then the waiver updates to recovered decreased by $120 and remaining increased by $120 within 5 seconds And an audit entry logs the reversal with references to P1 And reporting backfills to remove $120 from recovered totals for P1’s period Given payment P2 used for recovery is later refunded When the refund is posted Then the system automatically reduces recovered by the refunded recovery amount and reopens the waiver’s remaining accordingly, with audit entries for both events
Reporting totals, trends, and exports accuracy
Given waivers across a date range include both Recoverable (some partially/fully recovered) and Forgiven types When the revenue report is generated for the period Then it shows totals and monthly trends for recovered and forgiven amounts, plus outstanding recoverable remaining, matching ledger sums to the cent And client profile displays per-client totals: recovered, forgiven, and remaining, consistent with reports And the CSV export includes for each waiver: waiverId, clientId, type (Recoverable/Forgiven), waivedAmount, recoveredAmount, forgivenAmount, remainingAmount, invoiceId(s), lineItemId(s), recoveryPaymentId(s), timestamps, reason, and conversion/reversal references And exported totals reconcile exactly with on-screen totals
Retention Impact Analytics
"As an owner, I want to see whether waivers improve long-term retention so that I can justify compassionate policies with data."
Description

Deliver analytics that quantify the retention and revenue impact of waivers. Compute cohort-based retention curves, LTV deltas, and repeat-booking rates for clients who received waivers versus matched controls. Filter by reason, flag, service type, time window, and provider. Surface insights on a Waiver Impact dashboard and client profiles, with downloadable CSV. Apply privacy thresholds for small cohorts and label data freshness; ensure calculations are performant and incrementally updated.

Acceptance Criteria
Waiver Impact Dashboard: Cohort Retention Curves vs Matched Controls
Given a set of clients who received waivers and an eligible pool of non-waiver clients, When the Waiver Impact dashboard is opened with default filters, Then two retention curves (Waiver vs Matched Control) are displayed for 1, 2, 3, 6, 9, and 12 months post-index. Given retention is defined as the percentage of clients with at least one completed, non-cancelled session in the month bucket after the index date (waiver date for Waiver group; matched anchor date for Control), When the curves render, Then each plotted point uses this definition. Given a user hovers any retention point, When the tooltip appears, Then it shows group name, month bucket, numerator, denominator, and percentage to one decimal place. Given the matched control algorithm cannot reach at least 80% match coverage of the Waiver cohort size, When results render, Then a visible warning "Low match coverage" is displayed and the confidence band reflects reduced coverage. Given any group’s cohort size is below the configured privacy threshold, When computing curves, Then that group’s curve is suppressed and a badge states "Insufficient cohort". Given default filters are applied, When rendering the curves, Then P95 render time is <= 2.5 seconds and P99 <= 4 seconds for datasets up to 100k events.
Cross-Filter Segmentation by Reason, Flag, Service, Time Window, and Provider
Given the dashboard supports filters for Waiver Reason, Client Flag, Service Type, Time Window, and Provider, When a user selects values, Then all displayed metrics (retention curves, LTV delta, repeat-booking) recompute to reflect the selection. Given multiple values within a single filter (e.g., multiple reasons) are selected, When results compute, Then values are OR’d within that dimension and AND’d across different dimensions. Given a user applies any set of filters, When the page URL updates, Then the query string reflects the filter state and opening the URL in a new session reproduces the same view. Given a saved filter preset exists, When the user selects it, Then the same results and URL state are restored. Given filters result in no qualifying data, When the dashboard renders, Then a "No data" state is shown with zero metrics and no errors. Given filter changes, When metrics recompute, Then P95 update time is <= 2.0 seconds and P99 <= 3.5 seconds for datasets up to 100k events.
LTV Delta Calculation and Visualization
Given Lifetime Value (LTV) is defined as net revenue per client in the 12 months following the index date (gross charges minus waivers and refunds) in the organization’s currency, When analytics are computed, Then LTV is calculated for Waiver and Matched Control groups using this definition. Given LTV values for both groups, When displayed, Then the dashboard shows Waiver LTV, Control LTV, absolute delta (Waiver − Control), and percent delta with sample sizes and match coverage. Given confidence intervals are enabled, When LTV delta is displayed, Then a 95% CI is shown or a label "CI unavailable" if assumptions are not met. Given match coverage falls below 80% or CI cannot be computed, When rendering the LTV card, Then a warning badge explains the limitation and the value remains visible if privacy thresholds are met. Given a user hovers the LTV card info icon, When the tooltip appears, Then it shows the LTV formula, index date definition, and data window used.
Repeat-Booking Rate Analysis Post-Waiver
Given Repeat-Booking Rate is defined as the proportion of clients who book a second session within a selected window after the index date, When the default 90-day window is active, Then the dashboard shows Waiver and Control rates, absolute difference, and relative lift. Given window options of 30, 60, 90, and 180 days, When the user switches the window, Then metrics recompute and update within P95 <= 2.0 seconds. Given a user hovers the rate, When the tooltip is shown, Then it includes numerator, denominator, rate percentage, and the selected day window. Given a group’s cohort is below the privacy threshold, When computing rates, Then the value is suppressed and labeled accordingly.
Privacy Thresholds and Small-Cohort Suppression
Given a configurable privacyThreshold (default 20), When any metric is computed for a group with n < privacyThreshold, Then the metric is suppressed (displayed as "—") and a message "Suppressed due to small cohort" is shown. Given suppression is applied, When CSV export is generated, Then suppressed rows are omitted or aggregated such that no cell < privacyThreshold is present in the export. Given a user lacks the permission Analytics.ViewSmallCohorts, When viewing the dashboard or export, Then they cannot override suppression. Given the privacyThreshold is changed by an admin, When the change is saved, Then it is recorded in the audit log with actor, old value, new value, and timestamp.
Data Freshness Labeling and Incremental Updates
Given analytics are updated incrementally, When the dashboard loads, Then it shows "Last updated" timestamp in the org’s timezone and the data window (start and end dates). Given new waivers or bookings occur, When the incremental job runs, Then changes are reflected in the dashboard within 15 minutes at P95 and within 30 minutes at P99. Given incremental processing, When a partial update is in progress, Then users see a non-blocking "Updating…" badge without double-counting or temporary negative deltas. Given the pipeline is delayed beyond 60 minutes, When users view the dashboard, Then a visible "Data may be stale" warning is displayed. Given a daily full reconciliation job, When it completes, Then results are consistent with incrementals with discrepancies < 0.5% in aggregate totals.
CSV Export of Waiver Impact Analytics
Given a user has permission to export, When they click "Download CSV", Then a CSV is generated that reflects all current filters and segmentation. Given the export schema, When the file is generated, Then it contains columns for: metric_type, group, time_bucket/month, numerator, denominator, value, ci_lower, ci_upper, ltv_value, ltv_delta, repeat_rate, data_freshness_timestamp, currency_code, and applied_filters. Given privacy suppression rules, When exporting, Then rows that would expose cohorts smaller than the privacyThreshold are excluded or aggregated, and the file includes a note indicating suppression was applied. Given datasets up to 100k rows, When exporting, Then the file is ready for download within 10 seconds at P95 and 20 seconds at P99. Given numeric values, When written to CSV, Then percentages are to one decimal place, currency values to two decimals, and timestamps in ISO 8601.
Automation Triggers & Notifications
"As a freelancer, I want alerts when a client is nearing their waiver limit so that I can set expectations before the next session."
Description

Add waiver-related events (waiver_applied, waiver_limit_near, waiver_limit_exceeded, waiver_recovered) to SoloPilot’s automation engine. Allow admins to configure actions such as sending emails, creating tasks, tagging clients, or posting to Slack/webhooks. Provide merge fields (client, reason, remaining cap, invoice link) and per-rule throttling to prevent notification fatigue. Include in-app notifications and digest summaries, and expose event data via API for integrations.

Acceptance Criteria
Event Emission for Waiver Lifecycle
Given a waiver is applied to an invoice for a client in a workspace When the waiver is saved Then a waiver_applied event is produced within 2 seconds and persisted with event_id, event_type, workspace_id, client_id, invoice_id, waiver_id, waiver_reason_code, waived_amount, remaining_cap, actor_id, occurred_at (ISO 8601 UTC) Given a workspace has a per-client waiver cap C and a client’s usage becomes C-1 after applying a waiver When the waiver is saved Then a waiver_limit_near event is produced once per client per cap period with remaining_cap = 1 and includes cap_snapshot {cap: C, used: C-1} Given a client’s waiver usage equals C and an additional waiver is attempted and saved When the save completes Then a waiver_limit_exceeded event is produced with cap_snapshot {cap: C, used: C} and overage_count >= 1 Given a previously waived amount is recovered via payment or reversal When recovery is posted Then a waiver_recovered event is produced including recovered_amount > 0 and source_waiver_id Given the same waiver is re-saved or retried When the system processes it again with the same idempotency_key Then no duplicate events are produced
Rule Builder: Filters, Actions, and Test Fire
Given an admin creates a new automation rule When selecting the trigger Then the admin can choose waiver_applied, waiver_limit_near, waiver_limit_exceeded, or waiver_recovered Given an admin configures rule conditions When adding filters Then the admin can filter by client tags (include/exclude), waiver_reason_code (any of), remaining_cap <= N, waived_amount >= X, and workspace time window Given an admin chooses actions When configuring the rule Then the admin can add one or more actions: send email (to admin or client), create internal task, tag/untag client, post to Slack, or send generic webhook Given required fields are incomplete When attempting to save the rule Then the UI prevents save and displays field-level errors Given a configured rule When clicking Test Fire with a selected sample event Then previews render for each action and no external messages are sent
Merge Fields Rendering in Notifications
Given a rule action with a templated message When it runs on any waiver event Then merge fields render correctly: {{client.name}}, {{client.id}}, {{reason.code}}, {{remaining_cap}}, {{invoice.link}}, {{invoice.id}}, {{waiver.amount}}, {{waiver.id}}, {{workspace.name}}, {{occurred_at}} Given a message includes {{invoice.link}} When the recipient requires access control Then a signed URL is generated, valid for at least 24 hours, scoped to the recipient; if access cannot be granted, {{invoice.link}} is omitted and {{invoice.id}} renders instead Given a template references an undefined field When the action executes Then the field renders as an empty string without breaking delivery and the event is logged with a non-blocking warning
Per-Rule Throttling to Reduce Notification Fatigue
Given a rule has a throttle window W configured When multiple matching events occur for the same client within W Then only the first event triggers actions and subsequent events are suppressed Given suppression occurs When viewing rule execution logs Then the suppression count and most recent suppressed event timestamp are visible Given throttling configuration When editing a rule Then W can be set between 5 minutes and 7 days (default 24 hours), evaluated per rule per client per workspace Given throttling is active When a new matching event occurs after W has elapsed Then actions fire again and suppression count resets for that client and rule
In-App Notifications and Digest Summaries
Given a waiver event occurs When an admin user is logged into SoloPilot Then an in-app notification appears within 3 seconds showing event type, client, reason, remaining_cap, invoice link, and quick actions (view invoice, open client) Given an in-app notification is displayed When the user marks it as read or applies filters by type/date Then the notification state updates and persists; notifications remain retrievable for 90 days Given digest frequency is daily at 08:00 workspace local time (user can switch to weekly) When at least one waiver event occurred in the prior period Then a digest is generated and delivered containing counts by event type, totals for waived vs recovered revenue, and top 5 clients by event count; if zero events, no digest is sent Given suppression occurred due to throttling When the digest is generated Then suppressed counts per rule are included in the digest summary
Slack/Webhook Delivery and Retry Guarantees
Given a rule action posts to Slack When the action executes Then a message is posted to the configured channel with structured fields (client, reason, remaining_cap, invoice link) and deep links; 4xx responses surface a configuration error; 5xx/timeout responses trigger retry logic Given a generic webhook action with a signing secret When the action executes Then the request includes an HMAC-SHA256 signature header and a JSON body with the event payload; 429/5xx responses are retried with exponential backoff at ~1m, 5m, 15m (max 3 retries) before marking Failed Given delivery attempts are made When viewing delivery logs Then each attempt records request_id, timestamp, duration, response_code, and outcome with PII redacted Given Slack/webhook endpoints are unreachable for >15 minutes When retries are exhausted Then the system records a failure and raises an in-app alert for admins
Events API: Query, Security, and Pagination
Given an API token with admin scope When calling GET /api/v1/waiver-events with filters (type[], client_id, invoice_id, from, to) Then the response includes only events from the caller’s workspace, sorted by occurred_at desc, with cursor-based pagination (limit, next_cursor) Given a successful response When inspecting an item Then it includes event_id, event_type, occurred_at (UTC), client {id, name}, invoice {id, link}, waiver {id, amount, reason_code}, cap_snapshot {cap, used, remaining}, and action_outcomes {rule_id, action, status} Given rate limiting is enforced When exceeding 60 requests/minute per token Then the API returns 429 with a Retry-After header; legitimate requests below the limit succeed Given a non-admin token or cross-tenant identifiers When requesting events Then the API returns 403 and no cross-tenant data is exposed
Roles, Permissions, and Immutable Audit Log
"As an admin, I want a tamper-evident record of waiver decisions so that I can maintain compliance and trust."
Description

Define granular permissions to view/apply waivers, access private notes, override caps, edit reasons, and reverse waivers. Require elevated approval for actions exceeding policy thresholds, with configurable approver chains. Record an immutable audit log for every waiver event (who, when, where, what changed, justification, IP/device), present readable diffs, and support export for compliance. Implement soft-delete with tombstones and retention policies aligned with SOC 2/GDPR considerations.

Acceptance Criteria
Apply Waiver Within Role Permissions
Given a user with Apply Waiver permission on a client record, When they open an eligible invoice in Waiver Ledger, Then the Apply Waiver action is visible and enabled. Given a user without Apply Waiver permission, When they view the same invoice, Then the Apply Waiver action is not visible and direct API attempts return HTTP 403 with error code PERM_DENIED. Given a permitted user applies a waiver within the client’s policy cap and within their role limit, When the action is confirmed, Then the waiver is created, cap counters update, and an audit event is appended capturing who, when (UTC ISO 8601), where (workspace/module), what (amount, reason, note refs), IP, and device.
Access to Private Waiver Notes Is Restricted
Given a waiver has private notes, When viewed by a user without View Private Notes permission, Then the notes field is redacted in UI and API (notes=null, notes_status=redacted) and is excluded from user-scoped exports. Given a user with View Private Notes permission, When they view the waiver, Then the full private notes are visible and retrievable via API. Given any user without permission attempts to access private notes via direct API, When the request is made, Then the system returns HTTP 403 PERM_DENIED and no note content is leaked in error payloads.
Override Waiver Cap Requires Configurable Approval Chain
Given a policy threshold is exceeded by a requested waiver, And an approval chain is configured (e.g., Manager -> Owner), When a user with Override Cap permission submits the waiver, Then the waiver enters Pending status and approvers are notified in the configured order. Given the first approver approves within SLA, When the next approver acts, Then the request advances until final approval applies the waiver and updates balances; each approval/rejection appends an audit event. Given any approver rejects or the request times out per policy, Then the request is canceled with reason recorded and no waiver is applied. Given no approval chain is configured, When an over-threshold waiver is attempted, Then the system blocks submission and returns configuration error APPR_CHAIN_MISSING.
Edit Waiver Reason With Diff and Audit
Given a user with Edit Reasons permission, When they change the waiver reason from R1 to R2 within the editable window, Then the change is saved and the UI shows a readable diff (-R1, +R2) and the API exposes before/after fields. Given a user without Edit Reasons permission, When they attempt to modify the reason, Then the control is hidden in UI and direct API calls return HTTP 403 PERM_DENIED. Given the edit is saved, Then an immutable audit event is appended capturing actor, timestamp (UTC ISO 8601), previous_reason, new_reason, justification text, IP, device, and request_id; prior audit entries remain unchanged. Given the editable window has expired and policy requires approval for late edits, When a permitted user submits a change, Then the approval chain is invoked before persisting the update.
Reverse Waiver With Permission and Threshold Controls
Given a posted waiver exists, When a user with Reverse Waiver permission initiates reversal within the policy window and amount <= reversal threshold, Then the reversal is posted, financial counters are updated, and an audit event is appended; original audit entries remain intact. Given the reversal amount exceeds the threshold or the policy window has elapsed, When the user submits the reversal, Then approval is required; on approval the reversal posts, on rejection no changes are made. Given a user without Reverse Waiver permission, When they attempt reversal, Then the UI action is unavailable and API returns HTTP 403 PERM_DENIED.
Immutable Audit Log and Verifiable Export
Given any waiver event (create, update, approval, reversal, delete/tombstone), When it occurs, Then an audit record is appended-only (WORM) with fields: actor_id, actor_role, event_type, entity_ids, timestamp (UTC ISO 8601), previous_values, new_values, justification, IP, device/user_agent, origin, request_id, and hash. Given an administrator attempts to modify or delete an audit record, When the request is made, Then the system rejects the write with HTTP 403 IMMUTABLE_LOG and logs the attempt. Given an auditor with Export Audit permission requests an export for a date range in CSV or JSON, When the export runs, Then the system returns all matching records with a batch checksum and per-record hash, supports optional PII redaction (mask IP/device), and completes within 2 minutes for up to 100k records.
Soft-Delete With Tombstones and Retention Policies
Given a user with Delete permission deletes a waiver or associated note, When the action is confirmed, Then the record is soft-deleted, removed from default views, and a tombstone is created with id, type, deleted_at (UTC), deleter_id, reason_code, and retention_end_at; an audit event is appended. Given workspace retention is configured (e.g., 7 years) and a legal hold is not active, When retention_end_at is reached, Then PII is purged per policy, tombstone status updates to purged, and a purge audit event is appended. Given an export is generated during retention, When includeDeleted=true is specified, Then tombstoned records are included with their metadata; otherwise they are excluded by default. Given a GDPR erasure request is processed, When policy allows pseudonymization, Then personal identifiers in tombstones are pseudonymized while preserving compliance-required metadata.

Card Vault

Collects and securely stores a client’s payment method at booking, runs smart pre‑authorizations for deposits/fees, and auto‑updates expired cards. Enables instant capture for no‑shows and late cancels, eliminating collections work while keeping compliant and client‑friendly.

Requirements

Card-on-File Collection at Booking
"As a solo practitioner, I want to securely save a client’s card during booking so that I can charge deposits and fees without manual follow-up."
Description

Collect the client’s payment method during both self-serve and admin-created bookings and securely vault it via a PCI Level 1 payment gateway tokenization flow. Support SCA/3DS where required, perform $0 or minimal-amount card verification, and associate the vaulted token with the client profile and the scheduled session. Present clear consent text and store proof of authorization. Provide graceful fallbacks (secure pay link) if card entry is skipped or fails. Ensure masked display of card details, role-based access, and seamless reuse of the token for deposits, fees, and post-session invoicing. Integrate with SoloPilot’s scheduling and invoicing so the saved method is available for automatic capture at the appropriate lifecycle events.

Acceptance Criteria
Self-Serve Booking: Card Capture and Tokenization
Given a client starts a self-serve booking for a service requiring a card-on-file When the client reaches the payment step Then the card form renders via PCI Level 1 hosted fields preventing PAN/CVV from touching SoloPilot servers And SCA/3DS is initiated when required by issuer/region and completes successfully or frictionlessly And a $0 or $1 verification authorization succeeds and is immediately voided if applicable And the payment method is vaulted and a gateway token is returned And the booking confirms only after tokenization succeeds And the token is associated to the client profile and the scheduled session And the confirmation screen displays masked brand, last4, and expiry
Admin-Created Booking: Card Collection and Secure Pay Link Fallback
Given an admin schedules a session for a client without a saved payment method When the admin completes scheduling Then the system generates a secure pay link (TLS, one-time use) valid for 24 hours and tracks its status And the session remains Tentative until a card is added or the link expires And reminder notifications are sent at 12 hours and 2 hours before link expiry And on successful pay link completion, the card is SCA-authenticated as required, verified ($0/$1), vaulted, and associated to the client and session And users with Owner or Billing roles may alternatively collect the card in-app via hosted fields; no PAN/CVV is stored or logged
Consent and Proof of Authorization at Booking
Given a client is about to save a card during booking When the consent text is presented Then the language explicitly authorizes deposits, late-cancel/no-show fees, and post-session invoice charges And the client provides explicit affirmative consent before proceeding And the system stores an immutable authorization record including consent text version, timestamp, client ID, session ID, IP, user agent, gateway token/reference, and 3DS authentication result And the record is retrievable and exportable for disputes and audits
Verification and Decline Handling
Given a card is submitted for vaulting When the system performs a $0 or minimal-amount verification authorization Then on success the flow proceeds; on failure a clear issuer-derived error message is shown and the vaulting is blocked And the client may retry up to 3 times within 15 minutes before rate limiting applies And all attempts are logged with non-sensitive metadata And if verification fails or is skipped, a secure pay link fallback is offered and the session remains Tentative or auto-cancels after 24 hours per policy
Masked Display and Role-Based Access
Given staff view a client or session with a saved payment method When payment details are displayed Then only brand, last4, and expiry are shown; no full PAN or CVV is viewable or retrievable And only users with Owner or Billing roles can view masked details; Practitioners see only a "Card on file" indicator And all access to payment details is auditable with user, timestamp, and action And authorized roles can disable/remove tokens with a captured reason
Token Reuse for Deposits, No-Shows, and Post-Session Invoicing
Given a client has a vaulted payment method associated with a scheduled session When a deposit/pre-authorization is configured for the service Then a pre-authorization of the configured amount is placed at booking and its ID is stored And on late cancel/no-show, the system captures up to the policy-defined amount from the pre-authorization or stored token And on session completion and invoice finalization, the invoice balance is captured using MIT with stored 3DS data where applicable; on failure, retries follow the dunning schedule and notifications are sent And all authorizations/captures are idempotent and recorded on the session and invoice timelines
Association Integrity Across Scheduling and Invoicing
Given a card is successfully vaulted during booking When the client profile or session is updated, rescheduled, merged, or canceled Then the token remains linked to the client and session and is visible as a selectable payment source in scheduling and invoicing And rescheduling preserves the association; canceling releases or voids pre-authorizations per policy And merging duplicate clients consolidates tokens and preserves history without orphaning references And deleting a client removes tokens per data retention policy and revokes the gateway token
Smart Pre-Authorization Engine
"As a business owner, I want pre-authorizations to automatically match my cancellation policy so that holds and captures happen correctly without manual work."
Description

Implement a rules-driven engine to place pre-authorizations for deposits and policy-based fees at booking or within a configurable window prior to the session. Support fixed/percentage amounts, service-level overrides, client-specific exceptions, and multi-currency rounding rules. Manage hold lifecycle (create, refresh, release on timely cancel, or convert to capture on no-show/late-cancel) with idempotency to prevent duplicate holds. Handle reschedules by transferring or releasing holds per policy, and automatically expire unreleased holds. Surface failures with clear reasons and retry guidance, log all events for auditability, and integrate tightly with appointment state changes and SoloPilot’s invoicing automation.

Acceptance Criteria
Pre-Auth at Booking or Configurable Pre-Session Window
Given an organization rule sets pre-authorization timing to "at booking" and a client books a $100.00 USD session When the booking is confirmed Then the engine creates a pre-authorization within 2 seconds for the computed amount and returns a hold ID linked to the appointment Given an organization rule sets pre-authorization timing to "4 hours before session start" and an appointment reaches T-4h When the pre-authorization window opens Then the engine attempts the pre-authorization within 1 minute and links the hold to the appointment Given the booking event or pre-auth trigger is received more than once (e.g., webhook retry) When the engine processes the event with the same idempotency key Then no duplicate hold is created and the existing hold reference is returned Given a service is configured with pre-authorization disabled When a booking is created for that service Then no pre-authorization attempt is made and the decision is logged with reason "service_disabled"
Amount Calculation: Fixed, Percentage, and Multi-Currency Rounding
Given a service price of 120.00 USD and a deposit rule of 25% When computing the pre-authorization amount Then the amount is 30.00 USD rounded to the currency minor unit per platform currency rules Given a service price of 10,000 JPY and a deposit rule of 12.5% When computing the pre-authorization amount Then the amount is 1,250 JPY rounded to 0 decimals per JPY minor unit rules Given a fixed-deposit rule of €15.00 and a service price of €40.00 When computing the pre-authorization amount Then the amount is €15.00 Given a currency with 3 decimal minor units (e.g., BHD) and a 7.5% rule on 100.000 BHD When computing the pre-authorization amount Then the amount is 7.500 BHD rounded to 3 decimals per currency rules Given an amount is computed When the pre-authorization is created Then the authorization request uses the computed amount and currency exactly, and the computation inputs and result are logged
Rule Precedence: Service Overrides and Client Exceptions
Given an account default deposit rule of 20% and a service-level override of 10% When a booking is created for that service Then the 10% service-level rule is applied and the applied rule source is logged Given a client-specific exception "no deposit" When the client books any service Then no pre-authorization is attempted and the exception source is logged Given a client-specific exception of fixed $5.00 and a service-level 10% override When the client books the service priced at $100.00 Then the $5.00 client exception takes precedence and is used Given conflicting rules are present When the engine resolves the effective rule Then precedence order is: client exception > service-level override > account default, and the resolution decision is auditable
Hold Lifecycle: Create, Refresh, Release on Timely Cancel, Capture on No-Show/Late-Cancel
Given a pre-authorization has been created and the gateway hold will expire before the appointment When the remaining hold time is less than the configured refresh threshold Then the engine refreshes the hold (or re-authorizes per gateway capability) without creating duplicate holds and updates the hold reference Given an appointment is canceled within the free-cancellation window When the cancellation is processed Then the engine releases the hold within 2 minutes and records a release event Given an appointment is marked Late Cancel or No-Show per policy When the state change is processed Then the engine captures the authorized amount immediately; on success the appointment is marked "captured" and invoicing automation generates an invoice linked to the appointment and client within 10 seconds Given a capture or release event is retried When processed with the same idempotency key Then the operation is idempotent and no duplicate financial action occurs Given a gateway returns a non-success on refresh/release/capture When the engine receives the response Then the failure is logged with structured code and message, and the hold state remains consistent
Reschedule Handling: Transfer or Release per Policy
Given policy = Transfer hold on reschedule and an appointment with an active hold is rescheduled When the new appointment time is saved Then the hold remains active, is re-linked to the new appointment ID, and any scheduled refresh timers are updated to the new start time Given policy = Transfer hold and the new time exceeds the current gateway hold validity When evaluating the hold Then the engine refreshes/re-authorizes as needed to extend validity and voids the prior hold reference, without creating multiple active holds Given policy = Release-and-reauthorize on reschedule When the appointment is rescheduled Then the existing hold is released within 2 minutes and a new pre-authorization is scheduled per the timing rule for the new appointment Given repeated reschedule events occur When the engine processes them Then resolution is idempotent and results in a single active hold aligned to the latest appointment time
Automatic Expiry of Unreleased Holds
Given a hold has neither been captured nor explicitly released and has reached gateway expiry When the expiry is detected by the scheduler Then the hold is marked expired, no capture is attempted, and an expiry event is logged with gateway reference Given an expired hold exists linked to an upcoming appointment When determining next actions Then the engine follows the effective rule to either re-authorize at the configured pre-session window or skip if no deposit is required Given a hold is nearing expiry and policy forbids refresh When the engine evaluates the hold Then no refresh is attempted and the hold is allowed to expire with the outcome logged as "refresh_forbidden"
Failure Surfacing, Retry Guidance, and Audit Logging
Given a pre-authorization attempt fails due to insufficient funds When the engine returns the result Then the response includes a structured error code (e.g., "insufficient_funds"), human-readable reason, retryable=false, and guidance to collect a new payment method Given a gateway transient error occurs (e.g., network timeout) When the engine returns the result Then the response includes a structured error code, retryable=true, and a suggested retry-after in seconds Given any pre-authorization, refresh, release, transfer, or capture event occurs When the event completes Then the system logs an audit record containing timestamp, appointment ID, client ID, rule snapshot ID, action, amount, currency, gateway references, outcome, and correlation ID with sensitive PAN data excluded/masked Given an error is logged When viewing the audit trail Then the event includes enough context to reproduce the decision path (applied rule sources and values) without exposing secrets
Instant Capture for No-Shows & Late Cancels
"As a practitioner, I want fees to be auto-charged when a client no-shows or cancels late so that I don’t have to chase payments."
Description

Automatically capture the appropriate fee when an appointment is marked no-show or late-cancel. Prefer converting an existing hold; if none exists, perform a direct capture against the vaulted method per policy. Create or update the invoice, send itemized receipts, and notify both practitioner and client. Provide configurable grace periods, role-based overrides, and a failure pipeline (smart retries, alerts, and a secure pay link fallback). Ensure idempotency on repeated status changes, prevent double charges, and log structured evidence (timestamps, policy version, consent) to aid in disputes.

Acceptance Criteria
Capture from Hold with Optional Top-Up
Given an appointment has a valid pre-authorization hold and is marked No-Show by an authorized user When the status change is saved Then if the hold amount is ≥ the policy fee, convert the hold to a single capture for the exact policy fee amount And if the hold amount is < the policy fee, capture the full hold and immediately charge the remaining balance to the vaulted payment method And create or update a single invoice for the appointment with an itemized "No-Show Fee" line reflecting all related transactions And send an itemized receipt to the client and a notification to the practitioner within 2 minutes of successful capture(s) And store and link processor authorization and capture IDs on the invoice payment records
Direct Capture When No Hold Exists
Given an appointment has no valid pre-authorization hold and is marked Late Cancel or No-Show and the client has a vaulted payment method When the status change is saved Then charge the vaulted payment method for the policy fee in the account currency And create or update the invoice with an itemized fee line And mark the invoice Paid on success and send an itemized receipt to the client and a notification to the practitioner within 2 minutes And on payment failure, leave the invoice Unpaid and enter the failure pipeline
Configurable Grace Period Enforcement
Given grace periods for Late Cancel and No-Show are configured in minutes and each appointment has an associated timezone When an appointment is canceled or marked No-Show Then if the event timestamp is within the configured grace window, do not charge and do not create or update an invoice And if the event timestamp is outside the grace window, proceed with capture per policy And log the evaluated cutoff timestamp, timezone used, and decision outcome
Role-Based Override to Waive or Adjust Fee
Given a user with an allowed role (Owner or Admin) reviews a Late Cancel or No-Show before capture When the user applies an override to waive or adjust the fee Then require a mandatory reason note and enforce bounds (minimum 0, maximum policy fee) And reflect the overridden amount on the invoice and receipt and include audit details (user, role, timestamp, reason) And reject override attempts by unauthorized roles and perform no charge
Failure Pipeline with Smart Retries and Pay Link Fallback
Given a capture attempt fails and the decline is classified as retryable When the failure occurs Then schedule up to 3 retries at 6h, 24h, and 72h, attempting card updater before each retry And notify the practitioner on initial failure and on final failure And send a secure pay link to the client immediately; successful client payment reconciles the invoice, cancels pending retries, and sends receipt And if the decline is non-retryable, skip retries, send the pay link and alerts, and keep invoice Unpaid And log each retry attempt, outcome, and notification
Idempotency and Duplicate Charge Prevention
Given an appointment may receive duplicate status changes or webhook events for Late Cancel or No-Show When identical events are received for the same appointment within 30 days Then create at most one fee charge and one invoice line for the event And return the existing charge and invoice references for subsequent identical requests without charging again And if the event is reversed (e.g., No-Show to Attended), void or refund the fee per policy and update audit logs
Structured Evidence Logging for Disputes
Given any fee capture is attempted or completed When the system processes the event Then record a structured evidence entry including timestamp, appointment ID, client ID, actor ID, event type, policy version, consent snapshot, grace evaluation result, authorization/hold ID, capture ID, amount, and currency And make the evidence immutable, filterable by appointment and client, and exportable to PDF within 1 minute on request And include policy name and consent date references on the client receipt
Account Updater & Token Refresh
"As a business owner, I want expired or replaced cards to update automatically so that scheduled payments go through without interruptions."
Description

Leverage network account updater and gateway tokenization to automatically refresh expired or replaced cards without client friction. Schedule proactive refresh checks ahead of upcoming sessions, and update stored tokens and metadata upon success. When updater is unavailable or fails, notify the client with a one-click secure update link and remind them at smart intervals. Maintain audit logs of updates, surface updater success/failure rates in reporting, and never store raw PAN in SoloPilot’s systems.

Acceptance Criteria
Proactive Token Refresh Prior to Upcoming Session
Given a client has at least one scheduled session within 14 days and a stored card token eligible for update And the connected payment gateway supports network account updater When the refresh scheduler runs at 02:00 UTC Then the system submits an account update request for the card And if updated details are returned, the stored token and card metadata (expiry month/year, brand, last4, fingerprint) are updated within 60 seconds And no client notification is sent And an audit log entry is recorded with result=success And the next payment attempt for that client uses the refreshed token
Fallback Client Notification on Updater Failure or Unavailability
Given the gateway does not support updater or an updater attempt returns failure/not_found/unsupported And the card is linked to a client with a session within 14 days When the failure is recorded Then the system sends an email and SMS with a one‑click secure update link (single‑use, expires in 24 hours) And the link opens a PCI‑compliant hosted form without PAN traversing SoloPilot servers And reminders are sent every 48 hours until success or 24 hours before the session, whichever comes first, up to 3 reminders And upon client completion, the stored token and metadata are updated within 60 seconds And an audit log entry is recorded with result=client_updated
No Raw PAN Storage and Secure Token Handling
Given any flow that collects or updates a payment method When data is processed Then full PAN, CVV, and track data are never persisted in databases, logs, analytics, or crash reports And PAN never traverses application servers; only gateway‑hosted fields are used And only token, last4, brand, and expiry month/year are stored, encrypted at rest and in transit And role‑based access controls restrict token access to least privilege And automated scans run quarterly and report zero PAN pattern matches in storage And all exports and UIs redact PAN/CVV
Audit Log of Token Updates and Attempts
Given an account updater attempt or client‑initiated card update occurs When the operation completes (success, no_change, not_found, error) Then an immutable audit log record is created with: timestamp (UTC), client ID, payment method ID, related session ID (if any), gateway, action (updater|client_update), result, reason code, prior token fingerprint, new token fingerprint (if changed), prior expiry, new expiry, request ID, and actor (system|client) And records are write‑once, tamper‑evident, and retained for at least 24 months And logs are searchable by client ID and request ID with query latency ≤2s for up to 1M records
Updater Performance Reporting
Given audit logs exist for a selected date range When a user opens Card Vault > Reporting > Updater Then the dashboard shows: total attempts, successes, failures, no_change, success rate (success/(success+failure+no_change)), average latency, and top 5 reason codes And filters are available for date range, gateway, card brand, and upcoming‑session status And dashboard counts reconcile to audit logs within ±0.5% And users can export a CSV of the filtered dataset (≤100k rows) within 10 seconds And the report auto‑refreshes at least every 15 minutes
Idempotent Scheduler and Rate Limiting for Refresh Checks
Given the refresh scheduler runs daily at 02:00 UTC When evaluating eligible payment methods Then each card is attempted at most once per 7 days And duplicate job executions result in a single updater request per card per day via an idempotency key (token+date) And gateway rate limits are respected (configurable max 50 req/s) with exponential backoff starting at 1s up to 32s and up to 3 retries on 429/5xx And exhausted retries mark the attempt as error and re‑queue the card for the next eligible window And scheduler health metrics are emitted: processed, attempted, succeeded, failed, rate_limited
Use of Refreshed Token in Subsequent Charges
Given a successful account update for a client’s stored card has occurred And a payment attempt (invoice capture, late cancel, or no‑show fee) is initiated afterwards When the charge is created with the gateway Then the latest stored token (by fingerprint) is used And if the charge is declined due to token invalidation, the fallback notification flow is triggered within 5 minutes And authorization success rate for refreshed tokens is tracked and reported over a 30‑day cohort
PCI Compliance & Secure Storage Controls
"As an administrator, I want payment data handled in a compliant, secure way so that my business and clients are protected."
Description

Keep SoloPilot out of PCI scope for sensitive data by using gateway-hosted fields, tokenization, and redirect/iFrame collection that meets SAQ A. Enforce TLS 1.2+, encrypt all sensitive metadata at rest, and restrict access through RBAC and least privilege. Mask card details in UI, rotate secrets, and maintain detailed audit logs of access and payment events. Provide vendor attestations (PCI DSS Level 1), support SOC 2 alignment, and document incident response, key management, and data retention policies. Ensure regional compliance (e.g., SCA/PSD2) and publish a security overview for customers.

Acceptance Criteria
Gateway-hosted fields and tokenization (SAQ A scope)
Given SoloPilot renders a payment form for card collection When the client submits card details Then cardholder data is captured via gateway-hosted fields or a redirect/iFrame on the gateway domain And SoloPilot never receives, logs, proxies, or stores PAN, CVV, or track data at any point And the gateway returns only a token and masked metadata (last4, brand, expiry) which SoloPilot stores And Content Security Policy and Subresource Integrity prevent inline or custom card forms from loading And a data-flow diagram and SAQ A scope justification document are generated and stored in compliance records
TLS 1.2+ enforced for all payment and auth traffic
Given any HTTP(S) connection that captures, transmits, or accesses payment or authentication data When TLS negotiation occurs Then TLS 1.2 or 1.3 is required and TLS 1.0/1.1 and weak ciphers are rejected And HSTS (max-age >= 15552000; includeSubDomains; preload) is enabled for all production domains And certificate pinning is enabled in supported clients And SSL Labs scan reports overall grade A- or higher with no critical findings And downgrade attempts are blocked and logged with requestId and client IP
Encryption at rest with managed keys and secret rotation
Given sensitive payment-related metadata is persisted (e.g., tokens, last4, brand, billing zip, auth IDs, 3DS metadata) When the data is written to any storage or backup medium Then it is encrypted at rest using AES-256 (GCM or equivalent) with keys managed by a KMS/HSM And encryption keys and application secrets are rotated at least every 90 days or immediately upon suspected compromise And access to keys is restricted to the service role and all decrypt operations are auditable And database snapshots, object storage, and logs containing sensitive metadata are encrypted And decryption attempts are logged with actor, purpose, and requestId
RBAC least privilege and masked card details in UI
Given users with roles Owner, Admin, Staff, or Contractor access client payment methods When viewing payment method details Then only last4, brand, and expiry are displayed; full PAN/CVV are never visible or retrievable through UI or APIs And only users with the Payments:Manage permission can create preauths, captures, refunds, or delete payment tokens And unauthorized access attempts return HTTP 403 and are recorded with userId, role, IP, and requestId in audit logs And permission changes take effect within 60 seconds across services And support tooling masks PII by default with just-in-time reveal gated by explicit approval and logged
Comprehensive audit logging for access and payment events
Given payment or security events occur (token store/update, preauth, capture, refund, void, 3DS challenge, failed auth, login/logout, role change) When the event is processed Then an immutable audit log entry is recorded with ISO8601 UTC timestamp, actor, subject IDs, event type, result, reason codes, IP, user-agent, and requestId And logs are tamper-evident (hash chained or WORM), retained for at least 24 months, and accessible only with a Logs:Read permission And authorized admins can filter and export logs by date range, actor, client, and event type and receive results within 2 minutes And periodic integrity verification shows no gaps or checksum mismatches under normal operation
Vendor attestations and public security documentation
Given a PCI DSS Level 1 payment gateway and other critical vendors are used When reviewing compliance artifacts Then current PCI DSS Level 1 Attestation of Compliance and annual penetration test summaries are stored in a vendor registry with owners and expiry dates And SOC 2 report (or alignment memo) for SoloPilot is available, and a public Security Overview page is published and current And vendor data flows and subprocessors are documented and exposed via a machine-readable JSON endpoint And automated alerts notify owners 30 days before any artifact’s expiration
Regional compliance with SCA/PSD2 and 3-D Secure
Given a transaction with an EEA-issued card or within SCA jurisdictions When a preauthorization or capture is initiated Then 3-D Secure 2.x is invoked when required, supporting both frictionless and challenged flows And SCA exemptions (MIT, LVP, TRA) are applied via the gateway when eligible and the exemption type is logged And 3DS metadata (eci, transStatus, dsTransID, version) is captured and stored encrypted with the payment record And if SCA fails, no capture occurs; the booking is held, a clear error is shown, and retry paths are offered
Client Consent & Transparent Messaging
"As a client, I want clear information and control over stored cards and fees so that I feel safe and informed."
Description

Present clear, localized explanations of stored cards, holds, deposits, and potential fees at booking and in confirmations. Require explicit consent via checkbox with time-stamped capture of policy text and links to terms. Provide clients a portal to view/manage their saved methods, revoke authorizations, and update cards. Send timely notifications when holds are placed, released, or converted, and include itemized receipts for any capture. Ensure accessible UX (WCAG AA), multi-language support, and consistent messaging across web, email, and SMS.

Acceptance Criteria
Booking Consent: Explicit Checkbox, Snapshot, and Timestamp
- Given a client is booking a session, When the booking page loads, Then the consent checkbox is unchecked by default and must be checked to enable the Confirm/Book button. - Given the client checks the consent box and submits the booking, When the submission succeeds, Then the system persists a read-only consent record containing: client ID, booking ID, policy text snapshot, policy version ID, terms URLs, language/locale, client IP, user agent, UTC timestamp, and client-local timestamp. - Given a stored consent record, When an admin views the audit log, Then the exact policy text and links shown to the client are viewable read-only and exportable (PDF/JSON). - Given the client does not check the consent box, When they attempt to submit, Then the form is blocked with an accessible inline error and focus moves to the checkbox with error description.
Transparent Messaging: Localized Policy Copy at Booking and Confirmation
- Given locale detection or user-selected language, When the booking form renders, Then all policy and fee text, currency symbols, and date/time formats display in the selected locale. - Given merchant-provided translations exist for a language, When that language is selected, Then 100% of policy strings render localized with no mixed-language fragments. - Given a missing translation key, When rendering policy text, Then the system uses the configured fallback language and logs a missing-translation event with key and context. - Given booking completes, When confirmation page, email, and SMS are generated, Then the policy summary matches exactly the version and language shown at booking, including active links to terms and policies.
Accessibility: WCAG 2.1 AA Compliance for Consent and Messaging
- Given keyboard-only navigation, When interacting with the consent checkbox, policy links, and submit button, Then all controls are reachable in logical tab order and operable without a pointing device. - Given a screen reader (NVDA/JAWS/VoiceOver), When reading the consent section, Then control roles, names, states, and error messages are announced correctly using semantic HTML/ARIA. - Given visual presentation of consent and policy text, When displayed, Then color contrast is at least 4.5:1, focus indicators are visible, and text is resizable to 200% without loss of content or functionality. - Given automated accessibility scanning, When axe-core is run on booking and confirmation surfaces, Then there are zero critical or serious violations related to consent and policy messaging.
Client Portal: View, Update, and Revoke Stored Payment Authorizations
- Given an authenticated client, When opening Payment Methods, Then masked card details (brand, last4, expiry), authorization status, and any active holds are visible. - Given the client selects Update Card, When a new card is submitted successfully, Then the new method becomes default for future holds/captures and a confirmation notification is sent. - Given the client selects Revoke Authorization, When they confirm revocation, Then future holds/captures are blocked until new consent is granted, and both client and merchant receive notifications of the revocation. - Given authorization is revoked, When the client attempts a new booking, Then they are required to provide consent and a valid payment method before completing the booking.
Notifications: Holds Placed, Released, or Converted
- Given a hold is placed successfully, When the processor returns authorization, Then the client receives a notification via opted-in channel(s) within 2 minutes including amount, currency, reason, policy snippet, merchant descriptor, and hold expiration window. - Given a hold is released, When the release is confirmed, Then the client is notified within 2 minutes with amount, release timestamp, and guidance on bank reversal timing. - Given a hold is converted to a capture (e.g., no-show/late cancel), When the capture completes, Then the client receives an itemized receipt and the portal transaction updates within 2 minutes. - Given channel preferences, When the client has opted out of SMS, Then only email is sent and an in-portal notification is available; no SMS is delivered.
Itemized Receipts and Policy References for Captures
- Given a capture tied to a policy (no-show, late cancel, deposit), When a receipt is generated, Then it includes itemized lines, taxes/fees, total, policy name and version ID, booking reference, merchant descriptor, date/time in client timezone and UTC, and masked payment method (brand, last4). - Given a receipt is issued, When the client views it, Then they can download a PDF and access a web version that remains available for at least 18 months. - Given a dispute or inquiry, When staff opens the transaction record, Then the receipt and underlying consent record (snapshot and timestamps) are retrievable and exportable.
Cross-Channel Consistency and Policy Versioning
- Given a completed booking, When comparing web booking UI, confirmation email/SMS, and receipt, Then policy names, cancellation windows, amounts, and policy version IDs match exactly across channels. - Given a policy update, When a client books after the update, Then the new policy version is displayed and stamped to their consent; existing bookings retain and display their original policy version. - Given nightly integrity checks, When reconciliation runs, Then any mismatches between stored consent version and outbound messages are detected and flagged to admins for remediation.
Admin Policy Controls & Reporting
"As an owner, I want to configure fee policies and see hold/capture performance so that I can reduce losses and resolve disputes quickly."
Description

Offer a settings UI to configure global and service-level rules for deposits, hold timing, grace periods, late-cancel/no-show fees, and exemptions (e.g., VIP clients). Allow per-client overrides and temporary waivers. Provide a dashboard and exports for holds, releases, captures, declines, and aging authorizations, with filters by date, service, and client. Generate dispute-ready evidence packs containing policy snapshot, consent record, appointment logs, and communication history. Send alerts for expiring holds, high failure rates, and policy conflicts.

Acceptance Criteria
Global and Service‑Level Policy Configuration
- Given an Admin with Permissions:Payments opens Settings > Card Vault Policies - When they set a global deposit percentage P (0–100) and hold duration H hours (0–168) and click Save - Then the system validates bounds, persists values, returns 200 OK with a policyVersionId, and displays a success toast - And when a service override is created with deposit P2 and hold H2 - Then bookings created after save for that service apply P2/H2, while other services apply global P/H - And policy changes do not retroactively modify existing appointments or holds - And an audit log entry captures actor, timestamp, scope (global/service), and before/after values - And the public booking flow and /policies API return the effective policy snapshot (including policyVersionId) for a given service
Client‑Level Exemptions and Overrides
- Given an Admin opens a client profile - When VIP status is toggled ON - Then deposits and late/no‑show fees are waived for that client on future bookings unless a per‑client override explicitly sets different values - When a per‑client override (e.g., deposit 10%, fee cap $X) with expiry E and reason is saved - Then bookings created before E apply the override; bookings after E revert to effective service/global policy - And overrides cannot be backdated earlier than now; reason is required (10–250 chars) - And all changes are audit logged with actor and timestamp - And booking UI and API pricing consistently honors client‑level rules over service/global rules - And only Admin role can set VIP/overrides; unauthorized attempts are blocked with 403 and logged
Grace Period and Late/No‑Show Fee Enforcement
- Given a service configured with cancel grace period G minutes, late‑cancel fee F (amount or %), and no‑show fee N (amount or %) - When a client cancels at time T where T <= start_time − G - Then any existing pre‑authorization is released within 15 minutes and no fee is captured - When a client cancels at time T where T > start_time − G - Then the system captures fee F on the stored payment method, issues a receipt email to the client, and records a ledger entry - When an appointment is marked No‑Show (manual or auto at start_time + 15 minutes) - Then the system captures fee N; if the pre‑auth is expired, it performs one re‑auth then captures - On capture failure, the system retries up to 3 times over 24 hours; if still failing, it generates a payable invoice, notifies the provider, and records the failure in reporting - All actions (release, capture, retry, invoice) are appended to the appointment timeline with timestamps and transaction IDs
Hold/Capture Activity Dashboard, Filters, and Export
- Given an Admin opens Card Vault > Activity - When filters for Date Range [Start, End], Service(s), and Client(s) are applied and Search is executed - Then the list and summary tiles reflect Holds Created, Releases, Captures, Declines, and Aging Authorizations within the filter set - And the table shows Date/Time (workspace TZ), Client, Service, Appointment ID, Auth/Capture ID, Amount, Status, Aging Days; only card brand and last4 are displayed - And pagination supports at least 10,000 records; for up to 5,000 results the response time is <= 2 seconds at p95 - When Export CSV is requested - Then the file is generated within 30 seconds, matches the applied filters, includes on‑screen columns plus currency, and the totals reconcile with the dashboard - And the CSV filename includes workspace, date‑time, and a filter hash; download is authorized for Admin and Finance roles only
Dispute‑Ready Evidence Pack Generation
- Given an Admin views a captured late‑cancel or no‑show transaction - When Generate Evidence Pack is clicked - Then a ZIP is produced within 60 seconds containing: policy snapshot as of booking (policyVersionId), client consent record (timestamp, IP, user agent, consent text), appointment event logs, communications history (emails/SMS with timestamps and content), authorization and capture details (IDs, amounts, AVS/CVV result where available), and PDFs/screens of checkout and policy disclosure - And the pack includes a cover index, chronological ordering, and workspace timezone annotation - And the download link expires in 7 days, is accessible only to Admin/Finance roles, and each download is audit logged - And the generated pack is immutable; regenerating creates a new evidencePackId with full contents regenerated from source of truth
Expiring Hold Alerts and Aging Authorizations
- Given a scheduled job runs hourly - When it finds holds expiring within the next 24 hours or authorizations older than 5 days - Then an email and in‑app alert are sent to Admin listing client, appointment, amount, and expires_at; alerts are de‑duplicated with a 24‑hour cooldown per hold - And the Activity dashboard exposes an Aging Auths view sorted by days aging - And actions to Re‑auth or Release (subject to processor rules) are available with confirmation, are permission‑checked, and are audit logged - And holds that expire are auto‑marked Expired and excluded from future capture attempts
Policy Health Alerts: High Failure Rates and Conflicts
- Given the system monitors a 7‑day rolling window of authorization and capture attempts - When failure rate exceeds a configurable threshold (default 5%) with at least 50 attempts - Then an email and in‑app alert are sent to Admin with breakdown by service, trend, and suggested actions; the alert auto‑clears after 48 hours below threshold - And the Health panel shows current rates and thresholds - When an Admin attempts to save policies that are logically invalid (e.g., deposit > 100%, negative fees, grace period > hold duration, required fee missing) - Then the UI blocks Save, highlights offending fields, shows specific validation messages, and records the rejected attempt in the audit log

Cycle True-Up

Automatically reconciles each retainer at cycle end—calculating allowance used, overage, and underuse—then applies your carryover rules, generates the right invoice or credit memo, and sends a clear, client-friendly statement. Eliminates spreadsheet reconciliations, prevents disputes, and keeps both parties aligned month to month.

Requirements

Retainer & Carryover Rules Configuration
"As a solo consultant, I want to define my retainer and carryover rules once per client so that end-of-cycle reconciliation happens automatically and consistently."
Description

Provide an admin interface and rules engine to define retainer terms per client, including allowance type (hours/credits/$), cycle anchor and timezone, rate cards, overage rate calculation, underuse handling (carryover vs credit), carryover caps and expirations, rounding/increment rules, partial-cycle proration, cancellation/no-show policy mapping, and client/project-level overrides. Enforce validations to prevent contradictory settings, support templates and default policies, and version rules with effective dates so mid-cycle changes are traceable. Persist a rules snapshot with each reconciliation to ensure historical accuracy. Integrates with SoloPilot scheduling, time tracking, and invoicing modules to ensure consistent, reusable configuration across the platform.

Acceptance Criteria
Admin Rule Definition & Validation
Given I am an admin with permission to manage retainers, When I create a new retainer rule set and populate required fields (allowance type, cycle anchor, timezone, rate card, overage method, underuse policy, rounding/increment), Then the rule set is saved with a unique ID and visible in the client’s configuration. Given I attempt to save with contradictory settings (e.g., underuse policy set to both carryover and credit, rounding increment <= 0, negative rates, or missing timezone), When I click Save, Then the save is blocked and inline validation messages identify each invalid field. Given I set overlapping effective date ranges for the same client/project, When I click Save, Then I receive a conflict error that references the existing rule set and the save is blocked. Given allowance type requires a mapping (e.g., credits -> rate card mapping), When the mapping is missing, Then the system blocks save and specifies the missing mapping. Given templates and default policies exist, When I select a template, Then fields pre-populate accordingly and any unspecified fields fall back to system defaults as shown in the UI summary before save. Given project-level overrides are configured, When an override conflicts with a client-level rule, Then the system displays the precedence (project over client) and prevents circular or recursive overrides.
Cycle Anchor & Timezone Enforcement
Given a rule set defines a cycle anchor (e.g., 1st of month at 09:00) and an IANA timezone, When a cycle closes and the next begins, Then the system uses the defined timezone to determine cycle boundaries, correctly handling daylight saving transitions. Given sessions/time entries exist in mixed timezones, When reconciliation runs, Then all timestamps are normalized to the rule’s timezone before calculating allowance usage. Given an upcoming change to cycle anchor is saved with a future effective date, When the current cycle is active, Then the current cycle continues under the old anchor and the new anchor applies only from its effective date. Given the timezone is changed with a future effective date, When the next cycle starts, Then the new timezone is used for boundary calculations; historical cycles are unaffected.
Overage Calculation with Rate Cards & Rounding
Given rounded usage exceeds the configured allowance for a cycle, When reconciliation runs, Then overage_units = max(rounded_usage − allowance, 0) and an overage line item is generated using the configured overage method and the applicable rate card. Given project-level and client-level rate cards exist, When overage occurs for a project with its own rate card, Then the project rate card is used; otherwise the client default applies. Given rounding rule is set to per-entry rounding to a specified increment, When reconciliation runs, Then each entry is rounded to the increment before summation and allowance comparison. Given cancellation/no-show policy mapping exists, When entries flagged as cancellation/no-show are processed, Then billable units are included/excluded per the mapped policy before overage is calculated. Given invoicing integration is enabled, When overage is calculated, Then a draft invoice is created with itemized overage lines per project (if configured), linked to the cycle ID.
Underuse Carryover with Caps and Expiration
Given the cycle ends with underuse and the underuse policy = carryover, When reconciliation runs, Then carryover_units = min(allowance − rounded_usage, configured_carryover_cap) and a carryover record is created with an expiration based on the configured number of cycles or date. Given existing unexpired carryover and current-cycle usage, When applying usage, Then carryover is consumed first (FIFO) before current-cycle allowance, per the rules. Given carryover reaches its expiration, When reconciliation for the expiring cycle completes, Then expired units are removed and the client statement explicitly lists expired units and remaining carryover. Given carryover is disabled in the underuse policy, When reconciliation runs, Then no carryover record is created.
Underuse Credit Memo Generation
Given the cycle ends with underuse and the underuse policy = credit, When reconciliation runs, Then a credit memo is generated for underused_units valued at the configured underuse valuation rate and is linked to the cycle ID and client. Given credit application rules are configured, When the credit memo is generated, Then it is auto-applied to the next invoice or left unapplied according to configuration, and its status is reflected in the billing ledger. Given the valuation rate for credits is missing or invalid, When reconciliation is initiated, Then the run is blocked with an explicit configuration error and no financial documents are created.
Partial-Cycle Proration and Mid-Cycle Changes
Given a retainer starts or ends mid-cycle and proration is enabled, When the cycle is reconciled, Then the allowance is prorated using the configured method (e.g., calendar days in cycle) and rounded per proration rounding rules before comparison with usage. Given proration is disabled, When a retainer starts mid-cycle, Then the full allowance applies for that cycle. Given a rule change (allowance, rate card, rounding, policy) takes effect mid-cycle, When reconciliation runs, Then the cycle is segmented by effective dates and calculations (allowance, overage, underuse) are performed per segment, then aggregated into a single statement/invoice. Given a mid-cycle change removes a required mapping (e.g., credits without rate mapping) for the post-change segment, When saving the change, Then the system blocks save until required mappings are provided.
Versioning and Reconciliation Snapshot Integrity
Given an existing rule set, When I save changes with a new effective date, Then a new immutable version is created with version ID, author, timestamp, and effective date range; previous versions remain read-only. Given a reconciliation is executed, When it completes, Then a complete snapshot of the applied rule version(s) and values (including allowance type, anchors, timezone, rate card references, overage/underuse policies, rounding, proration, caps, expirations, overrides) is persisted with the reconciliation record. Given I view a past reconciliation, When I open its details, Then the system displays data calculated using the stored snapshot, not the current live rules. Given a rule version is referenced by any completed reconciliation, When a user attempts to edit that version, Then the system prevents modification and prompts to create a new version.
Automated Cycle-End Reconciliation Job
"As a busy practitioner, I want cycle reconciliation to run automatically at period end so that I never need spreadsheets to figure out usage and overage."
Description

Implement a reliable, idempotent background job that runs at each retainer’s cycle end (respecting client timezone) to aggregate actual usage, compare against allowance, compute overage and underuse, and apply carryover/credit rules. Handle mid-cycle starts/terminations via effective-dated proration, account for holidays/weekends, and gracefully process late data with a configurable cut-off. Produce a detailed reconciliation record with line items, applied rules, and totals. Include retry logic, observability (metrics, logs, alerts), and safeguards to prevent duplicate postings.

Acceptance Criteria
Cycle-End Execution Respects Client Timezone
Given a retainer with timezone TZ and cycle end on the last calendar day When local time reaches 00:00 at cycle end Then the job starts within 5 minutes and reconciles that cycle once Given multiple clients in different timezones When their cycle ends occur Then each client's job executes based on its own local time without cross-timezone drift Given a DST change occurs in TZ on the cycle end date When the cycle end boundary is reached Then the job runs exactly once at the correct local boundary Given cycle end falls on a configured non-business day (weekend/holiday) When deferral is enabled Then the job runs on the next business day; When deferral is disabled, Then it runs on the actual end date
Idempotent Reconciliation Prevents Duplicate Postings
Given the same retainer-cycle is processed more than once When a subsequent run executes Then no new invoices, credit memos, or reconciliation records are created and the run is logged as idempotent Given a prior attempt partially completed some steps When a retry occurs Then remaining steps complete without duplicating completed steps, using a stable idempotency key Given an existing reconciliation record with status Posted for that cycle When the job is triggered again Then it exits with no side effects and emits an idempotent no-op metric
Usage Aggregation, Allowance Comparison, and Reconciliation Record Generation
Given tracked billable items within the cycle When reconciling Then usage is aggregated by unit with two-decimal precision and excludes non-billable items Given allowance A and used U When computing Then overage = max(0, U - A) and underuse = max(0, A - U) Given computed values and applied rules When posting Then a reconciliation record is created with line items (allowance, used, overage, underuse, adjustments, carryover applied, credits) and totals that balance to the final amount due or credit
Proration for Mid-Cycle Start and Termination
Given a mid-cycle start date and full-cycle allowance A When reconciling the first cycle Then prorated allowance = A × (eligible days / cycle days) rounded per plan setting Given a termination effective date before cycle end When reconciling the final cycle Then prorated allowance and usage include only dates on/after start and on/before termination per configuration Given an effective-dated plan change within a cycle When reconciling Then the cycle is segmented by date ranges and each segment’s allowance is prorated and summed correctly
Carryover and Credit Rules Application
Given underuse and a carryover cap C with expiry of E cycles When applying rules Then carryover applied = min(underuse, C) and a balance with expiry E is recorded Given overage and available carryover balance When reconciling Then carryover is applied to reduce overage before invoicing per rule precedence Given a policy to issue a credit memo for underuse when carryover is disabled When reconciling Then a credit memo is generated for the configured rate/amount and linked to the reconciliation record
Late Data Handling with Configurable Cut-Off
Given a cut-off window of T hours after cycle end When usage is added within T Then it is included in the reconciliation; When added after T, Then it is deferred to the next cycle with an audit note Given a late reversal or deletion after the cut-off When processing Then an adjustment entry is created in the next cycle’s reconciliation record Given a manual include override for a specific late item When flagged Then it is included and the override is recorded in rules applied
Reliability: Retries, Observability, and Alerting
Given a transient failure such as a 5xx or timeout When detected Then the job retries up to N times with exponential backoff and jitter, recording retry counts in metrics Given a non-retryable validation error When encountered Then the job marks the reconciliation as Failed, emits an alert with retainer and cycle identifiers, and does not retry Given normal executions When running Then metrics (count, duration, success/failure, retries, idempotent no-ops) and structured logs (correlation id, retainer id, cycle id, idempotency key, rule decisions) are emitted; When failure rate exceeds threshold within 15 minutes, Then an alert is sent to the ops channel
Invoice and Credit Memo Generation
"As a consultant, I want the right invoice or credit memo created from the true-up so that billing is accurate without manual edits."
Description

Automatically generate the correct financial document from each reconciliation: invoice for overage, credit memo for underuse/credits, or no-op when balanced. Create clear line items (summary plus optional itemized entries), apply taxes/discounts, respect multi-currency settings, and follow numbering sequences. Post documents into SoloPilot’s invoicing, link them to the reconciliation record, and—based on client policy—auto-send or keep as draft. If a payment method is on file, attempt auto-collection with configurable retries and dunning. Ensure reversals/voids are possible only via controlled workflows with audit traceability.

Acceptance Criteria
Overage Generates Invoice
Given a retainer reconciliation where the final overage amount > 0 after applying carryover rules And the client account has an active billing profile When the cycle true-up runs at cycle close Then an invoice is created in SoloPilot invoicing for the overage amount And the invoice total equals the calculated overage plus applicable taxes minus applicable discounts And the invoice date equals the reconciliation close date And the document currency equals the client billing currency And the invoice references the retainer cycle period in the header
Underuse Generates Credit Memo
Given a retainer reconciliation where the final underuse credit > 0 after applying carryover rules And the client account supports credit memos When the cycle true-up runs at cycle close Then a credit memo is created in SoloPilot invoicing for the underuse amount And the credit memo totals reflect applicable taxes and discounts per configuration And the credit memo date equals the reconciliation close date And the document currency equals the client billing currency And the credit memo references the retainer cycle period in the header
Balanced Reconciliation No-Op
Given a retainer reconciliation where the final difference equals 0 after applying carryover rules When the cycle true-up runs at cycle close Then no invoice or credit memo is created And the reconciliation is marked as Balanced And an audit log entry records the no-op with timestamp and system actor
Line Items: Summary and Itemization
Given organization-level itemization settings are configured (summary-only or itemized) And a reconciliation produces an invoice or credit memo When the document is generated Then the document contains a summary line item describing cycle period and usage/credit summary And if itemization is enabled, additional line items are created for each included session/time entry with quantity, rate, and subtotal populated And line item subtotals sum exactly to the document subtotal And each line item carries correct tax codes and discount applicability flags per item configuration
Taxes, Discounts, and Multi-Currency
Given the client tax profile, item taxability, and discount rules are configured And the client billing currency is set and an exchange rate source is configured if cross-currency conversion is required When the document total is calculated Then taxes are computed per jurisdiction and rounding rules And percentage and fixed discounts are applied in the configured order (e.g., pre-tax or post-tax) And the document currency matches the client billing currency and amounts are rounded to that currency’s precision And the exchange rate (if used) and currency are recorded on the document
Document Lifecycle: Numbering, Posting, Linking, and Reversal Controls
Given invoice and credit memo numbering sequences are configured per legal entity/series And a reconciliation generates a financial document When the document is saved Then the next sequential document number from the correct series is assigned without gaps or duplicates And the document is posted into SoloPilot’s invoicing module and linked to the originating reconciliation record And reversal/void actions are available only via an authorized workflow requiring a reason and passing permission checks And performing a reversal/void creates immutable audit entries capturing user, timestamp, action, and affected document IDs And voiding is blocked if payments are applied; a reversal document is required instead with correct net effect
Auto-Send, Draft Policy, and Auto-Collection with Dunning
Given the client policy is configured for auto-send or draft And the client has a deliverable email address and optionally a stored payment method And auto-collection retries and dunning schedules are configured When the document is generated Then if policy is auto-send, the document is emailed with a client-friendly statement and delivery status is logged; otherwise it remains in Draft And if a valid payment method is on file and auto-collection is enabled, an immediate charge attempt is made for the outstanding balance And on payment failure, retries occur per the configured schedule until success or maximum attempts are reached And dunning notices are sent per schedule with statuses logged, and the document status updates to Past Due after the final failed attempt And on successful payment, the payment is applied to the document, the status updates to Paid, and a receipt is sent
Client-Friendly True-Up Statement Delivery
"As a client, I want a clear monthly statement so that I understand charges and credits and can approve payment quickly."
Description

Generate a branded, plain-language statement that explains allowance, usage, carryover applied, overage, and net charges/credits, with an optional legend for rules applied and adjustments. Support template customization, localization, and portal rendering alongside email delivery with attachments (invoice/credit memo). Enable preview before send, track delivery and opens, and provide a secure share link. Ensure accessibility compliance and mobile-friendly layouts to reduce client confusion and accelerate payment.

Acceptance Criteria
Accurate True-Up Statement Content Generation at Cycle Close
Given a retainer cycle has ended and time/usage entries are finalized When SoloPilot generates the True-Up statement Then the statement displays allowance, usage consumed, carryover applied, overage quantity and charges, underuse and credits, and net charges/credits with values computed from the cycle ledger And all monetary amounts are rounded per workspace currency precision and formatted per workspace settings And if "Include legend" is enabled, the statement contains a legend summarizing applied rules (carryover policy, overage rate, adjustments) with human-readable descriptions and references to the line items And if "Include legend" is disabled, the legend section is omitted And any line with a zero value is suppressed and replaced by a concise status note (e.g., "No overage this cycle") And labels use plain-language defaults, and internal field names do not appear in client-facing content And the net charges/credits total exactly equals the sum reflected on the generated invoice or credit memo for the cycle
Branding and Template Customization of True-Up Statements
Given workspace branding settings (logo, colors, typography) and a selected statement template When a statement is generated Then the statement header and footer render the configured logo, brand colors, and typography consistently across web and PDF And template sections (summary, detailed usage, legend, notes) can be toggled on/off per template and per send And custom text blocks (greeting, summary blurb, sign-off) support variables {client_name}, {cycle_period}, {due_date}, {amount_due} which resolve correctly And users can save at least 5 templates and assign a default per client or retainer And PDF export matches on-screen rendering within 2px layout tolerance and identical text content
Localization and Currency Formatting for Statements
Given a client language and locale preference exist (or workspace defaults) When a statement is previewed or sent Then all fixed copy and labels render in the selected language, dates format per locale, and numbers/currency format per locale (e.g., 1.234,56 € for de-DE) And the statement uses the retainer's currency code and symbol correctly on all monetary amounts And right-to-left languages render correctly with layout mirroring and preserved readability And missing translations fall back to the workspace default language and are logged for admin review
Email Delivery with Attachments and Portal Rendering
Given a statement is approved for sending and the client has at least one delivery email When the statement is sent Then the client receives a branded HTML email containing a secure link to the portal statement And the email includes the applicable invoice or credit memo as a PDF attachment and includes the statement PDF if configured to attach And if the net amount is zero, only the statement PDF is attached and no invoice or credit memo is attached And the client portal renders the statement identically to the preview and provides download links for the invoice/credit memo and statement PDFs And if email delivery hard-bounces, the send status updates to Bounced and the portal link remains accessible
Pre-Send Preview and Approval Workflow
Given a generated statement is in Draft When a user opens Preview Then the preview displays exactly what the client will see in the portal and PDF, including branding, localization, and toggled sections And the user can edit template text blocks for this send without altering the saved template and see changes reflected immediately And validation prevents sending if required recipient email, currency, or attachments are missing, showing actionable error messages And the user can send a test email to themselves that is flagged Test and not recorded as delivered to the client
Delivery, Open, and Download Tracking
Given statements are sent via email and available in the portal When delivery and engagement events occur Then SoloPilot records per-recipient send time, delivery status (Delivered, Bounced, Deferred), first open time, most recent open time, and unique opens count And portal view events and file download events (statement, invoice, credit memo) are recorded with timestamps And users can view an activity timeline for each statement from the retainer cycle with all events in chronological order And tracking respects client privacy settings; if tracking is disabled for a client, no open-pixel is injected and only delivery status is recorded
Secure Share Link, Accessibility, and Mobile-Friendly Layout
Given a statement is published When a secure share link is generated Then the link is a signed, unguessable HTTPS URL with at least 128 bits of entropy, scoped to the single statement And the link defaults to expire in 30 days, is revocable by staff at any time, and shows Expired after revocation or expiry And the statement page and PDFs meet WCAG 2.1 AA: sufficient color contrast, keyboard navigation, focus indicators, alt text for images, semantic headings, and tagged PDFs And on mobile screens 320–414 px wide, the statement renders without horizontal scrolling, with body text at least 16px and tappable targets at least 44x44 px And unauthenticated access is limited to the statement content only; attempts to access other resources are denied with 403
Approval & Exception Handling Workflow
"As a business owner, I want to review and override edge cases before invoices go out so that I can prevent disputes."
Description

Offer configurable thresholds for auto-approval (e.g., overage within X%) and a manual review queue for exceptions. Allow reviewers to adjust classifications, exclude anomalous entries, add explanatory notes, and rerun calculations prior to issuance. Support dispute flags, comment threads, and resolution actions that maintain a complete audit trail. Lock reconciliations after approval and document issuance, with role-based permissions and reminders/SLA nudges for pending items.

Acceptance Criteria
Auto-Approval Thresholds Enforcement
Given a retainer with auto-approval thresholds configured (percentage overage and/or absolute amount) When the cycle reconciliation completes Then if computed overage is within threshold and no exception rules are triggered, the reconciliation is auto-approved and the correct invoice or credit memo is generated without manual intervention And the approval actor is recorded as System with timestamp and the rule values used in the audit log And the client-facing statement includes an auto-approval note and a clear breakdown of allowance used, overage, and carryover applied
Exception Routing to Manual Review Queue
Given a reconciliation that violates thresholds or contains flagged anomalies When the cycle reconciliation completes Then the item appears in the Review Queue within 60 seconds with status Needs Review And the queue displays client, period, allowance used, overage/underuse, and exception reasons And only users with the Reviewer role or higher can open and act on the item; other roles have read-only access
Reviewer Adjustments and Exclusions
Given a reviewer opens an exception item When they reclassify entries between allowance, overage, non-billable or exclude entries with a reason Then totals recalculate in real-time and a delta summary (before vs. after) is shown And excluded entries are tagged with exclusion reason and user in the audit trail And if the financial impact exceeds a configurable threshold, a justification note is mandatory before proceeding
Recalculation and Preview Before Issuance
Given adjustments were made to a reconciliation When the reviewer selects Recalculate & Preview Then the invoice/credit memo preview updates within 5 seconds reflecting carryover rules and new totals And any validation errors (e.g., negative balance) are displayed with blocking status until resolved And the reviewer can rerun calculations multiple times with version history retained and visible
Dispute Flags, Comments, and Resolution
Given a statement has been issued When a client or internal user flags a dispute on one or more line items Then a dispute thread is created referencing the specific line items and amounts at time of dispute And participants can post comments with @mentions and attachments; all entries are timestamped and immutable And resolution actions (approve adjustment, reject dispute, split) update financials via adjustments and record resolution code, actor, and timestamp in the audit trail
Post-Approval Locking and Overrides
Given a reconciliation is approved and documents are issued When any user attempts to edit underlying entries or configuration for that cycle Then the system blocks the edit and displays Locked after issuance And only Admins with Override Lock permission can create corrective adjustments that affect a subsequent cycle; direct edits to the locked cycle remain prohibited And all override actions require a reason and capture before/after values in the audit log
Reminders and SLA Nudges for Pending Reviews
Given items remain in the Review Queue When an item exceeds the configured SLA (e.g., 2 business days) Then the system sends reminders to assigned reviewers and escalations to a manager per the escalation rules And reminders cease automatically once the item is approved or closed And the queue shows a timer and SLA status (On Track, At Risk, Breached) for each item
Usage Data Integration & Cut-off Controls
"As a practitioner, I want the true-up to use the right usage data with clear cut-offs so that nothing is missed or double-billed."
Description

Ingest and normalize usage from SoloPilot modules (scheduled sessions, time entries, deliverables) with deduplication, billable flags, and mapping to the correct retainer/project. Provide configurable cut-off windows for late entries, treatment of cancellations/no-shows, and handling of adjustments. Validate inputs (e.g., missing rates, unmatched activities) and surface an exceptions report so users can fix data prior to true-up. Ensure calculations are consistent with rate cards and rounding rules.

Acceptance Criteria
Ingestion, Normalization, and Deduplication
Given enabled modules (Sessions, Time, Deliverables) and an active retainer cycle When the ingestion job runs Then all new and updated items modified since the last successful run are imported and the run timestamp is recorded And each item is normalized to a canonical schema with required fields: source, external_id, client_id, project_id, retainer_id, user_id, activity_date, start_time and end_time or quantity, duration_minutes, billable_flag, status, created_at, updated_at And items with identical source+external_id are deduplicated by keeping the version with the latest updated_at And items lacking external_id are deduplicated using composite key (source, user_id, client_id, activity_date, start_time, duration_minutes), retaining only one version for valuation And deduplicated items are excluded from valuation and marked with reason 'duplicate'
Retainer and Project Mapping Rules
Given a normalized usage item requiring association to a retainer When mapping is performed Then if retainer_id is present on the item, that retainer is used And else if project_id is present, the retainer active for that project on the activity_date is assigned And else if only client_id is present and a client default retainer exists for the activity_date, that retainer is assigned And else the item is flagged as 'unmatched retainer/project' in exceptions And items mapped to a retainer from a different client are flagged as 'client mismatch' And exactly one retainer_id is assigned for every matched item
Billable Determination Including Cancellations and No-Shows
Given a usage item with type and status When the billable_flag is computed Then project/retainer billable rules are applied to set billable true/false And cancellations and no-shows are evaluated per policy: if policy is chargeable percent P, the item is billable with chargeable_duration = scheduled_duration * P and reason_code set; if policy is non-billable, billable_flag = false And user overrides require a reason_code and are audit logged with before/after values, actor, and timestamp And non-billable items appear in reports but do not consume allowance or produce invoice lines
Cut-off Window and Late Entry Handling
Given a per-retainer cut-off window defined as N days after cycle end in the tenant timezone When a usage item within the cycle is created or updated after the cycle end Then if updated_at <= cutoff_timestamp the item is included in the closing cycle And if updated_at > cutoff_timestamp the item is deferred to the next cycle and labeled 'late entry' And after cutoff_timestamp the cycle is read-only for usage; only adjustments may change the cycle balance And reopening a cycle requires admin permission and records an audit entry; ingestion and recalculation re-run on reopen
Manual Adjustments Workflow
Given a user with permissions creates an adjustment against a retainer cycle When the adjustment is saved Then required fields are provided: direction (credit/debit), units_type (time/value), amount, reason_code, memo, scope (allowance/overage/credit) And the adjustment is applied after usage valuation and before carryover rules in the true-up sequence And the adjustment appears on the cycle ledger, the generated invoice or credit memo, and the client statement with its memo And reversing an adjustment is done by posting an equal and opposite entry; original entries are immutable
Validation and Exceptions Reporting
Given normalized and mapped usage data When validation runs prior to true-up Then items violating rules are listed in an Exceptions report with category, item identifier, description, and a direct link to edit the source item And validation rules include: missing rate, unmatched retainer/project, client mismatch, negative or zero duration where not allowed, future-dated activity outside cycle, unknown status, duplicate detected, rounding config missing And the report shows counts by category and overall status (blocking/non-blocking) And true-up cannot be finalized while blocking exceptions exist; non-blocking warnings may proceed with user acknowledgement And rerunning validation after fixes removes resolved exceptions and updates counts
Rate Card and Rounding Consistency
Given configured rate cards and rounding rules for a retainer When valuing billable usage Then the rate is resolved in priority order: retainer-specific > project-specific > role > user > global default; missing rate triggers a blocking exception And time rounding is applied before valuation using the retainer's rule (nearest/ceil/floor) and increment (e.g., 6 or 15 minutes) And deliverables use unit rates without time rounding; minimum fee rules are applied if configured And currency amounts are rounded to 2 decimals using half-up rounding And each item amount equals rounded_units * resolved_rate and matches the amount on the generated invoice line
Audit Trail, Reporting, and Export
"As an operator, I want reports and an audit trail of true-ups so that I can answer client questions and reconcile accounting."
Description

Persist a versioned, immutable reconciliation record including inputs, rules snapshot, calculations, approvals, and financial documents. Provide reporting across clients and time ranges (overage revenue, outstanding credits, carryover liability, expiring carryover) with filters and drill-down to source entries. Enable CSV export and read-only API endpoints/webhooks (e.g., reconciliation.completed) for downstream accounting/BI systems. Ensure data retention and privacy controls align with platform policies.

Acceptance Criteria
Immutable, Versioned Reconciliation Record Persistence
Given a retainer cycle closes and reconciliation completes When the system persists the reconciliation Then a new reconciliation record is created with a unique id, version = 1, and read-only state And the record includes inputs snapshot, rules snapshot, calculation breakdown, approval metadata, and links to generated invoice/credit memo And a cryptographic hash of the record payload is stored and can be re-verified on retrieval And any adjustment or re-run creates a new version (version increments) without modifying prior versions And write attempts against an existing version return 403 and are audit-logged
Cross-Client Cycle True-Up Reporting with Filters
Given reconciliations exist across multiple clients and dates When a user runs the Cycle True-Up report for a chosen date range and client filter Then the system returns totals for overage revenue, outstanding credits, carryover liability, and expiring carryover And results can be filtered by client, plan, tag, and reconciliation status and sorted by any metric And totals match the sum of underlying reconciliations for the selected scope And the report renders within 5 seconds for up to 50,000 reconciliations
Drill-Down to Source Entries from Aggregates
Given a Cycle True-Up report result is displayed When a user selects a metric value to drill down Then a list of contributing reconciliations is shown with client, cycle, version, and amount columns And selecting a reconciliation shows its audit record and the exact source entries (sessions/time entries) used in calculations with quantities, rates, timestamps, and references And the sum of drill-down entries equals the selected aggregate value And missing or orphaned references are flagged and the user sees a data integrity warning
CSV Export of Reports with Deterministic Schema
Given a report result set is available When the user requests CSV export Then a UTF-8, RFC 4180-compliant CSV is generated with a header row and deterministic column order And numeric values use dot decimal with no thousands separators; dates/times are ISO 8601 in the tenant timezone And exports up to 100,000 rows are generated synchronously; larger exports run asynchronously with a secure download link emailed within 15 minutes And the exported totals match the on-screen report for the same filters and time range
Read-Only API Endpoints for Reconciliations and Reports
Given an authenticated API client with read permissions When the client requests GET /v1/reconciliations, GET /v1/reconciliations/{id}, or GET /v1/reports/cycle-true-up with valid filters Then the API returns 200 with paginated, filterable JSON including reconciliation id, version, integrity hash, amounts, document links, and timestamps And attempts to POST, PUT, PATCH, or DELETE these endpoints return 405 Method Not Allowed And responses include ETag headers and support conditional GETs via If-None-Match And requests are rate-limited to 120 requests per minute per token; excess requests return 429 with Retry-After
reconciliation.completed Webhook Delivery
Given a reconciliation completes (initial or new version) When webhooks are configured for reconciliation.completed Then an event is enqueued and delivered at-least-once to the subscriber endpoint with an HMAC-SHA256 signature header And the payload includes reconciliationId, version, clientId, cycleStart, cycleEnd, amounts (used, overage, underuse, carryover, credits), document links, and occurredAt And failed deliveries are retried with exponential backoff up to 8 attempts over 24 hours; deliveries are idempotent via eventId And users can view delivery logs and manually retry from the dashboard
Data Retention and Privacy Controls for Audit Trail and Exports
Given tenant retention and privacy policies are configured When the retention period elapses for webhook logs and derived exports Then logs and export files are automatically purged while reconciliation audit records remain immutable for the required retention period And access to audit records, reports, exports, and APIs is restricted to roles with Billing or Owner permissions, with all access events audit-logged And PII fields not required for reconciliation are excluded or redacted in records, reports, APIs, and exports by default And a legal hold flag prevents purging until the hold is cleared, with reasons recorded

Top-Up Orchestrator

When mid-cycle forecasts predict a shortfall, schedules and sends top-up invoices at your thresholds—optionally requiring client pre-approval—so work continues without pause. One-click checkout and auto-applied allowances keep delivery unblocked while improving cash flow and reducing end-of-month surprises.

Requirements

Shortfall Forecast Engine
"As an independent consultant, I want SoloPilot to predict when a client’s balance will run short so that I can proactively secure funds and avoid pausing delivery."
Description

Calculates mid-cycle funding shortfalls using real-time burn rate, scheduled sessions, contracted hours/value, open invoices, and client/project balances. Supports configurable forecast windows (e.g., next 7/14/30 days), multiple currencies, and per-client/project granularity. Produces threshold-based triggers that drive top-up creation, factoring in cancellations, reschedules, and pending invoices. Exposes forecast accuracy metrics and reason codes for transparency, and integrates with scheduling and invoicing services to ensure predictions update instantly as plans change.

Acceptance Criteria
Configurable Forecast Windows (7/14/30 Days) Produce Accurate Shortfall Estimates
Given organization forecast windows configured to 7, 14, and 30 days And client/projects with real-time burn rate, scheduled sessions, contracted hours/value, open invoices, and current balances When the forecast engine runs Then it returns shortfall values for each window per client/project And aggregate organization-level totals for each window are provided And each shortfall reflects: current balance + open and pending invoices - expected spend from burn rate and scheduled sessions within the window, constrained by contracted hours/value And values match a reference calculation within 0.5% or 0.01 in the local currency (whichever is greater)
Real-Time Forecast Updates on Scheduling Changes
Given an existing forecast snapshot for a client/project When a session is created, edited (duration or rate), cancelled, or rescheduled in the scheduling service Then the affected forecast recomputes within 5 seconds And the forecast version increments and stores a change log entry with reason code SCHEDULE_CHANGE and the session ID And the recomputed shortfall reflects the change in the correct forecast window(s)
Multi-Currency Forecasting with Base-Currency Rollups
Given projects in multiple currencies and a base currency configured And an exchange rate source and timestamp configured for FX conversion When forecasts are generated Then each project forecast is presented in its native currency And client/organization rollups are presented in the base currency using the configured FX rate and timestamp And values are rounded to 2 decimal places per currency standard And a reason code FX_RATE_APPLIED is recorded with the rate and timestamp used And if the FX rate updates, the rollup recomputes within 5 seconds and records reason code FX_RATE_CHANGE
Per-Client/Project Threshold Triggers for Top-Up Creation
Given threshold rules configured per client/project (percentage remaining and/or absolute balance) When a forecasted balance within a configured window is projected to drop below the threshold Then the engine emits a single trigger event with client/project ID, window, threshold matched, suggested top-up amount, and reason code THRESHOLD_BREACH And no trigger is emitted if the forecasted balance stays at or above the threshold And the engine does not emit duplicate triggers for the same client/project/window/threshold until the forecasted balance rises above the threshold and later breaches again
Open and Pending Invoices Reduce Projected Shortfall
Given open invoices (unpaid, sent) and pending invoices (draft or awaiting approval) tied to a client/project When the forecast engine computes shortfall Then open and pending invoice totals are credited against the forecasted spend for the relevant window(s) And each credit is traceable via reason codes OPEN_INVOICE_APPLIED and PENDING_INVOICE_APPLIED with invoice IDs And if an invoice is paid or cancelled, the forecast updates within 5 seconds and removes or adjusts the credit with reason code INVOICE_STATUS_CHANGE
Cancellations and Reschedules Adjust Forecasted Spend
Given scheduled sessions within a forecast window When a session is cancelled as non-billable per policy Then the session's expected spend is excluded from the forecast within 5 seconds and reason code CANCELLATION_NON_BILLABLE is recorded When a session is cancelled as billable per policy Then the session's expected spend remains included with reason code CANCELLATION_BILLABLE When a session is rescheduled outside the window Then its expected spend is moved to the new window in the forecast with reason code RESCHEDULE_OUT_OF_WINDOW
Forecast Transparency: Accuracy Metrics and Reason Codes
Given a forecast generated for a window When the window elapses and actuals are finalized Then the engine calculates and stores accuracy metrics per client/project/window, including absolute error, percentage error (MAPE), and coverage (percentage of spend explained by modeled components) And users can retrieve the metrics and the forecast breakdown with reason codes for contributing components (e.g., BURN_RATE, SCHEDULED_SESSIONS, OPEN_INVOICES, PENDING_INVOICES, FX_RATE, CANCELLATION_ADJUSTMENT) And accuracy metrics are available within 24 hours of window close
Threshold & Policy Configuration
"As a solo operator, I want to set clear rules for when and how top-ups are created and sent so that the system behaves predictably for each client."
Description

Provides an admin UI and API to define top-up thresholds and orchestration policies per client/project: trigger types (remaining balance, days of coverage, percentage of retainer), top-up amount calculation (fixed, to target balance, or runway days), minimum/maximum invoice amounts, rounding rules, and bundling behavior. Allows setting whether client pre-approval is required, delivery windows, timezone-aware send schedules, channels (email/in-app), default templates, currency/tax region, and fallback actions if approval/payment is not received. Includes permissioning and validation to prevent conflicting rules.

Acceptance Criteria
Threshold by Remaining Balance with Target Balance Top-Up (UI/API parity)
Given an Admin creates a policy for client "Alpha" project "A" with trigger type "Remaining Balance <= 500 USD" and top-up calculation "To Target Balance 2,000 USD", currency "USD", tax region "CA", and default invoice template set via the Admin UI And the policy is retrievable via the public API When the forecast detects remaining balance = 450 USD at 10:00 in the project's timezone Then a top-up invoice is generated for 1,550 USD with CA tax applied according to the configured tax region And the invoice is linked to project "A", uses currency USD, and uses the policy's default invoice template And the API returns the policy with fields matching the UI configuration (trigger, calculation, currency, tax region, template id)
Days of Coverage Trigger with Runway Calculation
Given an Admin configures a policy for client "Beta" project "B" with trigger type "Days of coverage < 5" and amount calculation "Runway to 10 days" And the average daily burn is calculated as 300 USD/day over the last 14 days When the remaining balance is forecast as 900 USD (3 days of coverage) Then the system generates a top-up invoice for 2,100 USD to restore 10 days of runway post-payment And the resulting projected coverage after payment is 10 ± 0 days according to the burn calculation method
Min/Max Invoice Amount and Rounding Rules Enforcement
Rule: Compute raw top-up amount, then apply the configured rounding rule, then enforce min/max bounds by clamping to [min, max]. Example A: Raw = 257.43, Rounding = "round up to nearest 5", Min = 250, Max = 1,000 → Rounded = 260 → Final = 260. Example B: Raw = 248.01, Rounding = "round up to nearest 5", Min = 300, Max = 1,000 → Rounded = 250 → Clamped to Min → Final = 300. Example C: Raw = 999.01, Rounding = "round up to nearest 5", Min = 100, Max = 1,000 → Rounded = 1,000 → Within bounds → Final = 1,000. Then the generated invoice amount equals the Final amount and is persisted identically in UI, API, and accounting export.
Bundling Behavior for Multiple Pending Top-Ups
Given bundling is enabled with a 24-hour bundling window per project and line-item aggregation by trigger reason When two top-up triggers for project "C" compute $120 at 09:00 and $80 at 16:00 local on the same day Then the system issues a single top-up invoice totaling $200 with two line items reflecting each trigger And if a third trigger of $50 occurs after the 24-hour window, it is sent as a separate invoice And the bundled invoice total respects the policy's rounding and min/max rules applied to the aggregate amount
Client Pre-Approval and Fallback Actions
Given a policy requires client pre-approval with approval timeout 48h and payment timeout 72h after approval, fallback action "pause delivery", and channels "email + in-app" with a default approval template When a top-up is computed under this policy Then an approval request is sent immediately via email and in-app using the configured template with variables (client name, project, amount, due date) populated And if the client declines, no invoice is sent and the project owner is notified And if not approved within 48h (project timezone), the system executes the fallback action "pause delivery" and records an audit log entry And if approved but not paid within 72h, the system executes the configured non-payment fallback and notifies the project owner
Timezone-Aware Send Schedule and Delivery Windows
Given a policy with timezone "America/Los_Angeles" and a send window Mon–Fri 09:00–17:00 local When a trigger occurs Friday at 18:30 local time Then the approval/invoice send is queued and dispatched Monday at 09:00 local time And during DST transition, a trigger at 02:10 local time on the spring-forward date is dispatched at 09:00 local time the same day And all scheduled times are stored and displayed in both UTC and the project timezone
Permissioning and Conflict Validation
Given roles Admin and Member exist When a Member attempts to create or modify a top-up policy via UI or API Then the request is rejected with HTTP 403 and the UI shows "You do not have permission to edit Top-Up policies" When an Admin attempts to save a policy that conflicts with an existing policy for the same project (e.g., duplicate trigger type with overlapping thresholds or overlapping send schedules) Then the save is blocked and a validation error is shown/returned with HTTP 409 and error code "POLICY_CONFLICT" listing the conflicting fields And successful creates/updates/deletes by Admin are audit-logged with actor, timestamp, and before/after values
Automated Top-Up Invoice Generation
"As a coach, I want top-up invoices to be generated and sent automatically with credits applied so that billing stays accurate without manual effort."
Description

Automatically creates draft top-up invoices when thresholds are met, then sends according to policy. Prefills line items, rates, taxes (VAT/GST), and memo context linking to forecast triggers. Auto-applies allowances, prepayments, and credits before computing the requested amount, ensuring clients are not overcharged. Generates secure payment links, supports localized templates and numbering, and schedules send times respecting client timezones and quiet hours. Supports invoice bundling to reduce email noise and adheres to accounting sync rules.

Acceptance Criteria
Auto-Draft Creation on Threshold Breach
Given an engagement with an active top-up policy and a configured shortfall threshold And the mid-cycle forecast shows a shortfall equal to or greater than the threshold When the forecast evaluation job runs Then the system creates exactly one draft top-up invoice within 60 seconds of detection And the draft links to the engagement, forecast snapshot ID, and triggering policy And duplicate drafts for the same shortfall are suppressed for 24 hours unless the shortfall increases by at least 10%
Prefilled Line Items, Rates, Taxes, and Trigger Memo
Given a draft top-up invoice is created When the invoice is generated Then line items are prefilled with quantity, unit, and rate derived from the forecast delta And applicable VAT/GST is calculated using the client's tax profile and service jurisdiction And tax amounts and totals are rounded using the account’s rounding rules And the memo includes a link to the forecast trigger and a human-readable reason
Correct Application of Allowances, Prepayments, and Credits
Given the client has active allowances, prepayments, or credits And a draft top-up invoice requires an amount to be computed When the invoice amount is calculated Then the system applies allowances first, then prepayments, then credits And the requested amount never exceeds the remaining shortfall after these applications And if the net amount is less than or equal to zero, no invoice is sent and an audit log entry is created
Client Pre-Approval Gate for Top-Up Invoices
Given the top-up policy requires client pre-approval When a draft top-up invoice is created Then the client receives a secure approval request with invoice preview And the invoice remains blocked from sending until approved And approval sets the invoice to scheduled according to send rules And rejection or expiry after N calendar days notifies the account owner and cancels the draft
Timezone-Aware Send Scheduling and Quiet Hours with Bundling
Given the client’s timezone and quiet hours window are configured And the policy is set to auto-send upon approval or when no approval is required When a draft becomes eligible to send Then the send time is scheduled to the next allowed window in the client’s local time And if multiple eligible drafts exist for the same client within the bundling window (e.g., 24 hours), a single email is sent bundling all invoices And no email is sent during quiet hours And the system records the actual send timestamp and delivery status
Secure Payment Link and One-Click Checkout
Given an invoice is sent When the client opens the invoice Then a TLS-secured, tokenized payment link is present with a configurable expiry of at least 30 days And one-click checkout pre-fills amount, currency, and billing details And on successful payment, the invoice status updates to Paid within 10 seconds of gateway confirmation and a receipt is emailed
Localization, Numbering, and Accounting Sync Compliance
Given the client locale, currency, and numbering scheme are configured When generating the invoice document and metadata Then the template, language, date format, and currency symbols match the client locale And invoice numbering follows the account’s sequence rules without gaps or duplicates And the invoice is formatted to satisfy VAT/GST requirements for the jurisdiction And upon finalization, the invoice syncs to the connected accounting system following mapping rules; on sync failure, the invoice is retried up to 3 times and the owner is alerted
Client Pre-Approval Workflow
"As a therapist, I want clients to approve top-ups in advance when required so that expectations are clear and disputes are minimized."
Description

Implements an optional approval step before sending or charging a top-up. Sends a branded approval request via email and in-app, with mobile-friendly review, itemized breakdown, and optional comments. Supports expirations, reminders, partial approvals (within policy), change requests, and a complete audit trail (who/when/what changed). Defines fallbacks for non-response (auto-cancel, auto-send, or escalate) and integrates with e-sign or checkbox attestation for compliance when required.

Acceptance Criteria
Branded Pre‑Approval Dispatch on Forecasted Shortfall
Given a forecasted top‑up shortfall meets the configured threshold and pre‑approval is enabled When the orchestrator generates a pre‑approval request Then an email is sent to the client within 2 minutes and an in‑app notification appears within 5 seconds And the email and page use the account’s branding (logo, name, primary color) And the request status becomes "Pending Approval" and is visible on the request and client timeline And the approval link is single‑use, includes a secure token, and honors the configured expiration And opening an expired or already‑used link shows an "Invalid or expired" message and provides a path to request a new link And if the email bounces, the request is flagged "Undeliverable" and the account owner is notified
Mobile‑Friendly Review with Itemization and Comments
Given a client opens the approval link on a mobile device (viewport ≤ 414 px) When the approval page renders Then no horizontal scrolling is required and all CTAs are fully visible without overlap And font sizes are ≥ 14px and tap targets are ≥ 44x44 px And the page displays an itemized breakdown (description, qty, rate, subtotal, tax/discounts/allowances, total) And the computed total equals the sum of line items ± tax/discounts within $0.01 And an optional comment field is available When the client submits a comment with or without an approval decision Then the comment is saved, time‑stamped, attributed to the client, and added to the audit log And the provider receives a notification containing the comment
Partial Approval Within Policy Limits
Given the organization policy allows partial approvals with a minimum of 50% of the requested total or a $200 floor (whichever is higher) When the client enters a partial amount ≥ the policy minimum and ≤ the requested amount Then the system accepts the partial approval, sets status to "Partially Approved," and recalculates the approved total accordingly And the invoice/top‑up preview updates to reflect the approved amount and preserved line‑item proportions When the client enters a partial amount below the policy minimum Then the approval action is blocked with an inline error explaining the minimum required amount Given policy disables partial approvals When the client attempts to enter a partial amount Then the partial input is hidden or disabled and only full approval is permitted
Change Request Negotiation Loop and Versioned Audit Trail
Given a client proposes changes (amount or allowed line‑item fields) instead of approving When the client submits a change request Then the request status becomes "Change Requested" and the provider is notified with the proposed diffs When the provider counters or accepts Then a new version is created with an incremented version number and a diff of who/when/what changed And prior versions become read‑only and are labeled as superseded And the audit log captures user, timestamp, IP, and field‑level changes for every version When a version is approved by both parties Then the workflow finalizes to status "Approved" and no further edits are permitted without creating a new request
Expiration, Reminders, and Fallback Handling
Given a pre‑approval request has a configured expiration of 72 hours and a reminder cadence of 24 hours When the request is created Then a countdown is displayed to the client and reminders are sent at T‑24h and at expiration via email and in‑app And no more than 2 reminders are sent and duplicate reminders are suppressed after a response When the request reaches expiration without response and fallback = Auto‑Cancel Then the request status becomes "Expired — Cancelled," no invoice/charge is sent, and notifications are dispatched to both parties When fallback = Auto‑Send Then the system sends the invoice/initiates the charge, sets status to "Expired — Auto‑Sent," and records the system action in the audit log When fallback = Escalate Then status becomes "Expired — Escalated," the internal escalation group is notified, and no invoice/charge is sent And all fallback outcomes are logged with actor = System, timestamp, and configuration snapshot
Compliance Attestation Capture (E‑Sign / Checkbox)
Given the client or engagement requires compliance attestation When the approval page loads Then a mandatory e‑sign pad or compliance checkbox and consent text are presented before approval can be submitted When the client completes an e‑signature and approves Then the system stores the signature image, typed name, timestamp, IP address, user agent, and a hash of the approved content, and generates a signed PDF receipt linked in the audit log When the client consents via checkbox and approves Then the system stores the consent text version, timestamp, IP address, user agent, and a hash of the approved content, and links the record in the audit log And changes to consent text create a new version; existing approvals retain their original version linkage
Optional Pre‑Approval Toggle Behavior
Given pre‑approval is disabled for the client or this top‑up request When a top‑up threshold is met Then the system bypasses approval, sends the invoice/initiates the charge immediately, and records that pre‑approval was skipped by configuration Given pre‑approval is enabled When a top‑up threshold is met Then the invoice/charge is blocked until approval status is Approved or Partially Approved, unless fallback = Auto‑Send after expiration When the account admin changes the default pre‑approval setting Then the change applies only to new requests, requires admin permissions, and is recorded in the audit log with who/when/what changed
One-Click Checkout & Payment Methods
"As a freelancer, I want clients to pay top-ups in one click using their preferred method so that cash flow is fast and work remains unblocked."
Description

Delivers a frictionless payment page for top-ups with one-click checkout for saved methods and support for cards, ACH/SEPA, and digital wallets (Apple Pay/Google Pay). Handles SCA/3DS, retries on soft declines, and configurable dunning. Displays fees and settlement timelines transparently, issues receipts, and posts real-time payment webhooks. Supports client authorization for future auto-charges aligned to policy, with secure tokenization and PCI-compliant handling via the payment provider.

Acceptance Criteria
One-Click Checkout with Saved Method (No SCA)
Given the client has a saved default payment method and SCA is not required When the client clicks “Pay Now” on the top-up page Then the payment is submitted without any additional inputs And a success confirmation is displayed within 5 seconds And the charged amount equals the top-up amount minus auto-applied allowances and matches the invoice And the payment record reflects the correct currency, amount, and method used
SCA/3DS Challenge Handling
Given the issuer requires SCA/3DS for the transaction When the client initiates payment Then a 3DS challenge is presented via the provider modal without page reload And upon successful challenge, the payment completes and the success screen is shown within 5 seconds And upon failed or abandoned challenge, an actionable error is shown with options to retry or choose another method And all SCA outcomes are logged with correlation IDs for audit
Payment Methods Availability and Eligibility
Given workspace payment configurations, client region, and device/browser capabilities When the top-up page loads Then eligible methods (Cards, ACH for US, SEPA for EU/EEA, Apple Pay/Google Pay where supported) are displayed and ineligible methods are hidden And digital wallet buttons render within 2 seconds and prefill amount and merchant details correctly And ACH/SEPA options display mandate/authorization text and estimated clearing timelines before confirmation And selecting any displayed method proceeds to capture required fields via the provider’s secure elements
Fee and Settlement Transparency
Given fees may be passed through to the client and settlement timelines vary by method When the client reviews the payment summary Then the UI displays itemized lines: Top-up amount, Allowances/Discounts, Processing fee (if applicable), and Total due And an estimated settlement date/ETA is shown per method based on provider SLAs (e.g., Cards T+2, ACH/SEPA T+3–5) And currency formatting matches the workspace currency minor units and equals the transaction record And when fees are passed through, the client must affirm a fee acknowledgment checkbox before paying
Soft Decline Retries and Configurable Dunning
Given a payment attempt receives a soft decline response code When the decline is detected Then the system retries automatically once within 60 seconds unless additional authentication is required And further retries follow the workspace-configured dunning schedule (attempt count, intervals, and notifications) And hard declines are not retried automatically and prompt the client to choose another method And all retries are idempotent and never create duplicate charges And client and workspace notifications use the configured templates and only send on state change
Receipts and Real-Time Webhooks Delivery
Given a payment succeeds When the provider confirms success Then a receipt email is sent to the client and workspace billing email within 60 seconds including amount, fees, net, method last4/type, transaction ID, invoice reference, and business details And a payment.succeeded webhook posts within 10 seconds with signed payload containing gross, fee, net, method type, mandate/authorization status, settlement ETA, and idempotency key And webhook delivery retries on 5xx with exponential backoff for up to 24 hours and records delivery attempts And on failures, a payment.failed webhook is emitted; receipt is not sent unless explicitly configured
Client Authorization and Tokenization for Future Auto-Charges
Given the workspace policy allows future auto-charges When the client checks out Then a clear authorization statement is displayed and requires explicit opt-in (checkbox) before storing the method And upon opt-in, the payment method is tokenized by the provider; no PAN or sensitive data is stored on SoloPilot servers (SAQ A scope) And the stored method/mandate ID is linked to the client and usable for future top-ups without re-entry unless SCA is required by issuer And if SCA is required for setup, a mandate authentication flow is completed and recorded And the client can revoke authorization from their portal, disabling future auto-charges immediately
Balance Reconciliation & Service Unblocking
"As a SoloPilot user, I want paid top-ups to immediately increase a client’s available balance so that I can continue sessions without manual intervention."
Description

On payment or approval (per policy), updates the client/project balance, allocates funds to the correct retainer or workstream, and unblocks scheduling or deliverables that were pending due to low funds. Supports partial payments, proration of available hours, and automatic reattempts for previously blocked automations (e.g., session-to-invoice). Creates immutable ledger entries, syncs to external accounting, and handles reversals for refunds/chargebacks while maintaining audit integrity.

Acceptance Criteria
Immediate Balance Update on Payment or Approval
Given a client/project has a pending top-up triggered by low funds and allocation rules are configured When payment is captured or a policy-based approval is recorded Then the client and project balances increase by the net top-up amount within 10 seconds And funds are allocated to the correct retainer(s)/workstream(s) per line-item mapping rules and currency And an immutable ledger entry is created with type=credit, amount, currency, source (payment/approval), related retainer/workstream IDs, prior_balance, new_balance, actor, and UTC ISO 8601 timestamp And duplicate notifications (e.g., webhook retries) do not change balances more than once (idempotent processing)
Service Unblocking upon Restored Balance
Given a project’s scheduling and deliverables are blocked due to balance below threshold T When the new balance equals or exceeds T from a payment or approved top-up Then scheduling controls and related API endpoints are re-enabled within 5 seconds And the unblock event is logged with reason="funds restored" referencing the triggering ledger entry And any attempt to schedule immediately after unblock returns success (HTTP 200) assuming availability rules are met
Partial Payment Proration of Available Hours
Given a retainer with hourly rate R and available_hours H, and a partial payment of amount A is allocated When proration is computed Then available_hours increases by (A/R) rounded to the nearest minute, stored at >=4 decimal places and displayed to 2 decimals And any multi-workstream allocation splits hours proportionally to the monetary allocation per line item And any residual amount insufficient to add one minute is retained as monetary balance and not discarded And available_hours does not exceed a configured cap; any excess remains as monetary balance
Automatic Reattempt of Blocked Automations
Given automations (e.g., session-to-invoice, reminders) previously failed with block_reason="insufficient funds" When the balance is restored above threshold T Then each unique blocked automation is reattempted within 15 minutes And reattempts are idempotent (no duplicate invoices/sessions are produced) And failures on reattempt are logged with error details and notify the workspace owner And up to 3 retries occur with exponential backoff before marking as permanently failed
Immutable Ledger and External Accounting Sync
Given a ledger entry (credit or debit) is finalized for a payment, approval, refund, or chargeback When persistence completes Then the entry becomes immutable (no updates; only linked reversals allowed) And the entry is synced to the connected accounting system within 60 seconds with correct account mapping, tax treatment, currency, and a unique external reference And sync failures trigger retries with exponential backoff for up to 24 hours and surface a visible "Failed" sync status after 3 attempts And idempotency keys prevent duplicate external records; reconciliation status shows Pending/Synced/Failed
Refunds and Chargebacks Reversal Handling
Given a refund or chargeback event occurs for a prior top-up When the event is received Then a reversal ledger entry (type=debit) is created linking to the original credit and allocated across the same retainer/workstream proportions And client/project balances are decreased accordingly; if balance falls below threshold T, scheduling and deliverables are blocked within 5 seconds with reason="insufficient funds" And the reversal is synced to the external accounting system as an appropriate document (refund/credit note/chargeback) referencing the original And no historical entries are deleted or modified; the audit trail remains intact
Concurrency and Ordering Guarantees
Given multiple payment/approval notifications arrive concurrently or out of order for the same client/project When processing completes Then the total applied balance equals the sum of unique successful events only (no double counting) And per-event processing uses idempotency keys and transactional locks to avoid race conditions And the system reaches a consistent state within 30 seconds; any temporary mismatch is resolved by a reconciliation job And each applied event is traceable by a correlation ID across ledger, allocations, and external sync logs
Notifications & Escalations
"As a consultant, I want clear notifications and escalation paths around top-ups so that I never miss critical funding events and can act quickly."
Description

Sends timely, configurable alerts to internal users and clients at threshold detection, approval request, invoice sent, payment received, and delinquency. Supports multi-channel delivery (email, in-app, Slack), quiet hours, timezone awareness, and smart batching to reduce noise. Provides escalation paths for unapproved or unpaid top-ups (e.g., notify account owner, pause scheduling, or switch to smaller incremental top-ups). All events are logged to the activity timeline for traceability.

Acceptance Criteria
Threshold Detection: Configurable Alerts with Quiet Hours and Timezone
Given a forecasted shortfall crosses the configured threshold, when detection occurs, then an alert is generated within 60 seconds and targeted to recipients per notification preferences. Given recipient quiet hours are active in their configured timezone, when an alert is generated, then delivery is deferred until the next allowed window and the original event timestamp is preserved in the message. Given multiple threshold crossings occur for the same client within the configured batch window (e.g., 15 minutes), when alerts are sent, then a single consolidated alert is delivered summarizing all items. Given duplicate events (same eventId) are emitted within 24 hours, when notifications are processed, then only one alert is delivered (idempotent) and subsequent duplicates are logged but not re-sent. Given multi-channel is enabled, when the alert is sent, then it is delivered via each recipient’s selected channels (email, in-app, Slack) with channel-specific templates and consistent content. Given a Slack destination is configured, when the alert is delivered, then it posts to the correct workspace and channel, mentions the assignee if configured, and includes deep links to the top-up and client record. Given any channel fails (e.g., 5xx from provider), when sending the alert, then the system retries with exponential backoff up to 3 times and falls back to the next available channel, marking the failed channel in logs. Given an alert is generated, when delivery completes, then an Activity Timeline entry is created with event type, recipients, channels, delivery status, and correlationId.
Approval Request: Client Pre-Approval Multi-Channel with Smart Batching
Given a top-up requires client pre-approval, when the threshold is crossed, then a pre-approval request is sent to the client via their preferred channels with Approve and Decline actions and an authenticated one-click link. Given multiple approval-requestable items are created within the batch window, when messaging occurs, then one consolidated approval request is sent listing each item with amount, currency, and justification. Given the client approves via any channel, when the action is received, then approval is recorded within 2 seconds, all channels reflect the approved state, and the orchestrator proceeds to create the invoice. Given the client declines, when the action is received, then work on related items is marked as blocked, the account owner is notified, and an Activity Timeline entry records the decline reason. Given no response within the configured SLA (e.g., 24 hours), when the timer elapses, then an escalation notice is sent to the account owner and the client is reminded once (no more than 1 reminder per SLA window). Given quiet hours are active for the client, when reminders are scheduled, then the send time respects the client’s timezone and quiet hours configuration. Given link security is required, when the client opens the one-click approval link, then they are validated via signed token (exp ≤ 24h) and see the approval context with amount, rate/allowance, and service scope.
Invoice Sent: Client Notification and Activity Timeline Logging
Given a top-up invoice is generated, when it is sent, then the client receives a notification containing invoice number, amount, currency, due date, and a one-click checkout link. Given the client’s preferred channels are configured, when the invoice is sent, then messages are delivered via those channels and recorded as Delivered or Failed per channel with timestamps. Given the invoice is sent, when logging occurs, then a Timeline entry is created capturing invoiceId, correlationId, recipients, channels, and the message preview hash (no PII). Given multiple invoices are sent within a 10-minute window to the same recipient, when batching is enabled, then the client receives one digest message listing each invoice and total. Given the invoice is re-sent, when the same invoiceId is notified within 2 hours, then the message is marked as Re-send in logs and the client receives an updated subject indicating Re-sent, not a duplicate new invoice. Given the checkout link is clicked, when the page loads, then the top-up allowance and any auto-applied credits are pre-populated and the amount due matches the invoice total to the cent.
Payment Received: Confirmation Notifications and Auto-Resolve Escalations
Given payment for a top-up invoice is received, when the payment is confirmed by the processor webhook, then a confirmation notification is sent to the client and internal assignee within 60 seconds. Given any open escalations or reminders exist for the invoice, when payment is confirmed, then all pending escalations for that invoice are auto-cancelled and marked Resolved in the Timeline. Given quiet hours are active for the client, when sending confirmations, then the client receives the confirmation after quiet hours, while internal users receive in-app immediately and email per their quiet hours. Given partial payment is received, when notifications are sent, then the remaining balance and new due date (if any) are clearly displayed and logging reflects Partial rather than Paid in full. Given payment fails or is reversed, when the processor posts a failure event, then a failure notification is sent to internal recipients, the client is informed with a retry link, and the invoice returns to Unpaid state with updated Timeline entries.
Delinquency Escalation: Notify Account Owner and Pause Scheduling
Given an invoice is past due beyond the configured first escalation threshold (e.g., 48 hours), when the timer elapses, then an escalation notification is sent to the account owner and client with status Delinquent and next steps. Given the invoice remains unpaid at the second threshold (e.g., 72 hours), when the timer elapses, then scheduling for the client is automatically paused (if the policy is enabled) and internal users are notified of the pause with a resume-on-payment rule. Given scheduling is paused by escalation, when payment is received, then scheduling automatically resumes and a Resolution notification is sent to all prior escalation recipients. Given quiet hours, when any escalation is due during quiet hours, then the client-facing escalation is deferred, but the internal owner receives an in-app alert immediately and email per their quiet hours. Given all escalation actions, when they occur, then entries are appended to the Activity Timeline with actor=System, action, timestamp, and prior state for auditability.
Delivery Reliability: Deduplication, Retries, and Channel Failover
Given the system processes notification events, when two events share the same correlationId and type within 24 hours, then only one notification is sent and the second is dropped with a dedup reason in logs. Given a provider returns transient errors (HTTP 5xx, timeouts), when sending, then the system retries with exponential backoff (e.g., 1s, 4s, 16s) up to 3 attempts per channel before failing over to the next configured channel. Given all configured channels fail, when delivery cannot be completed, then the notification is marked Failed with reason, a task is created for the assignee, and a Timeline entry records failure details for traceability. Given rate limiting is required, when more than N notifications target the same recipient within an hour, then additional notifications are consolidated into a digest per channel without losing critical severity messages. Given a DST transition occurs for a recipient’s timezone, when scheduling messages around quiet hours, then send times are computed using timezone-aware calendaring so that quiet hours remain consistent across the DST change. Given security policies are enforced, when notifications are generated, then no sensitive payment tokens are included, links are signed and expire, and all message bodies pass content linting for required fields (client, amount, due date, link).

Client Usage Portal

Gives clients a secure, shareable view of real-time usage vs. allowance, upcoming sessions, projected run-out date, and invoices due. Builds transparency, cuts status back-and-forth, and speeds approvals for top-ups or scope changes with embedded action buttons.

Requirements

Secure Client Access & Sharing Controls
"As a client contact, I want a secure, shareable portal link with granular access controls so that I can view my account without risking unauthorized exposure."
Description

Implements a secure, client-facing portal with passwordless magic links (with optional OTP), expiring shareable URLs, and role-based visibility controls per client contact. Supports SSO (Google/Microsoft) where configured, link revocation, and granular section toggles (usage, invoices, sessions). Ensures encryption in transit and at rest, masks sensitive fields by default, and logs access attempts. Integrates with SoloPilot’s client directory to inherit contacts and permissions, and with branding settings to present a white-labeled experience. Outcome: clients can safely access their data without creating friction for the solo operator.

Acceptance Criteria
Passwordless Magic Link Authentication with Optional OTP
Given a client contact with portal access enabled and OTP disabled, when they request a magic link, then an email is sent within 60 seconds containing a single-use link that expires at the configured time. Given a valid, unexpired magic link, when the contact clicks the link, then they are authenticated and redirected to their portal home without entering a password. Given a magic link that has been used or has expired, when it is clicked again, then access is denied and an "expired or invalid link" message is displayed. Given OTP is enabled for the contact, when the contact clicks a valid magic link, then they are prompted for a one-time code sent to their email; entering a correct, unexpired code grants access and incorrect/expired codes deny access. Given any magic link request or use, when processed, then an audit log entry is recorded with timestamp, contact identifier, delivery channel, and outcome.
SSO Login via Google and Microsoft (When Enabled)
Given SSO is enabled in workspace settings, when a contact selects "Continue with Google/Microsoft" and successfully authenticates with a matching email for an active client contact with portal access, then they are logged in and redirected to the portal. Given SSO is disabled in settings, when a contact views the sign-in screen, then SSO buttons are not displayed. Given a contact attempts SSO with an identity whose email does not match an active client contact with portal access, then access is denied and the attempt is logged with provider and outcome. Given any SSO attempt completes (success or failure), when it occurs, then an audit log entry records provider, timestamp, resolved contact (if any), and outcome.
Shareable URL Expiration and Single-Use
Given a shareable portal link with a configured expiration timestamp, when the link is accessed before expiry, then it authenticates according to configuration (direct or OTP challenge) and grants access. Given the same link is accessed after its expiration timestamp, then access is denied and an expiration message is shown; no portal data is returned from APIs. Given the link is configured as single-use, when it has been used once to authenticate, then subsequent attempts with the same URL are denied. Given any access using a shareable URL, when processed, then a log entry records link identifier, timestamp, IP, user agent, contact (if resolved), and outcome.
Link Revocation and Immediate Access Termination
Given an active shareable link exists, when the solo operator revokes the link in admin, then new attempts to use that link are denied within 60 seconds of revocation. Given an active session established via a now-revoked link, when revocation occurs, then that session is invalidated and redirected to the sign-in screen within 60 seconds. Given a link is revoked, when the action is saved, then an audit entry records actor, link identifier, timestamp, and optional reason.
Permissions Inheritance and Role-Based Visibility per Client Contact
Given a client contact in the SoloPilot client directory has assigned portal permissions for sections (Usage, Sessions, Invoices), when the contact authenticates, then only the assigned sections are visible and accessible; unassigned sections are hidden and their APIs return 403. Given the solo operator updates a contact’s permissions in the client directory, when the contact next loads the portal, then visibility reflects the updated permissions without requiring reinvitation. Given a contact has no sections assigned, when they authenticate, then the portal displays a "no sections available" state and no data endpoints are exposed. Given any permission evaluation occurs server-side, when a request is made to a restricted endpoint, then the decision is enforced and logged.
Granular Section Toggles: Usage, Sessions, Invoices
Given workspace-level section toggles are configured for a client, when a section is set to off, then the section is not rendered in the portal and related API endpoints return 404/disabled for all contacts under that client. Given a section toggle is changed from off to on, when a contact refreshes the portal, then the section appears with data within 60 seconds and respects the contact’s permissions. Given a section toggle is changed, when the change is saved, then configuration history records actor, timestamp, section, and new value.
Security, Masking, Audit Logging, and White-Label Branding
Given a client accesses the portal, when network requests are made, then all requests are served over HTTPS and HTTP is redirected or blocked; HSTS is enabled for the portal domain. Given portal data is persisted, when reviewing storage configuration, then encryption at rest is enabled for all client portal data stores. Given sensitive fields exist (e.g., internal notes or fields flagged sensitive), when rendered in the client portal, then they are masked by default or omitted unless the contact’s role explicitly permits viewing; any reveal action is user-initiated and logged. Given any authentication or access attempt occurs, when it completes, then an audit log entry is persisted with timestamp, subject/contact, method (Magic Link, OTP, SSO, Shareable URL), IP, user agent, and outcome. Given branding settings (logo, colors, organization name) are configured, when the portal and sign-in screens render, then configured branding is applied and default SoloPilot branding is not displayed.
Usage & Allowance Engine
"As a consultant, I want the portal to show real-time usage against the client’s allowance so that clients can self-serve status and avoid back-and-forth."
Description

Calculates real-time consumption against plan allowances (hours, sessions, or credit-based packages) with configurable rules for rounding, carryover, proration, and non-billable exclusions. Pulls completed and scheduled sessions from SoloPilot Scheduling, includes time/notes where applicable, and reconciles against invoices to prevent double counting. Supports multiple active bundles per client, effective dates, and retroactive corrections. Presents a clear meter with remaining balance and breakdown by engagement/package, enabling transparent, self-serve status checks.

Acceptance Criteria
Real-Time Usage Calculation Across Plan Types
Given a client has active allowances of types hours, sessions, and credits with plan-specific rounding rules When the engine ingests completed sessions with durations and notes from Scheduling Then consumed totals and remaining balances are updated per plan type within 15 seconds of session completion And the configured rounding rule is applied per plan to the recorded duration/quantity And raw and rounded quantities are stored per consumption record And API responses reflect rounded consumption and reconcile to the plan balance
Carryover and Proration Handling
Given a plan has carryover enabled with a maximum carryover limit and an expiry window When a billing cycle closes with unused allowance Then up to the configured limit is carried over and marked with the configured expiry date And carried-over amounts expire and are removed from the balance after the expiry date Given a plan starts mid-cycle with proration enabled When the plan activates on its effective date Then the initial cycle allowance is prorated according to the configured rule and effective start date
Non-Billable Exclusions from Consumption
Given a session, task, or time entry is flagged as non-billable in Scheduling or Notes When the engine ingests the record Then it is excluded from allowance consumption and does not decrement any plan balance And the exclusion reason and source flag are stored on the consumption record for auditability
Invoice Reconciliation and Double-Counting Prevention
Given a session or time entry has already decremented an allowance and an invoice is generated referencing that usage When the engine reconciles invoice lines with consumption records Then the invoice line is linked to the underlying consumption record and no additional consumption is deducted And ad-hoc invoice lines not linked to a session/time entry do not affect allowance balances And if an invoice is voided or a line is credited, the linked consumption is released back to the allowance (subject to bundle eligibility at the original service date) without creating duplicates
Multiple Active Bundles Allocation with Effective Dates
Given a client has multiple active bundles with defined effective date ranges and a configured allocation priority (e.g., earliest-expiring first) When a session is completed whose service date falls within more than one eligible bundle Then the engine allocates consumption to the eligible bundle according to the configured priority And if the selected bundle balance is insufficient, the remainder is allocated to the next eligible bundle in priority order And bundles whose effective date window does not include the service date are never debited
Retroactive Corrections and Recalculation with Audit Trail
Given a past session is edited (duration change, billable flag change, reassignment to a different engagement) or a bundle is added/updated with a backdated effective date When the change is saved Then the engine recalculates consumption from the effective change point, updates affected bundle balances, and re-resolves allocation according to current rules And the client-facing meter reflects updated balances within 15 seconds And an immutable audit log entry is recorded with before/after values, user, timestamp, and impacted records
Meter and Breakdown Presentation for Client Portal
Given the client portal requests the usage meter for a client When the engine responds Then the payload includes, per engagement/package: allowance total, consumed (actual), scheduled (reserved), projected (actual + scheduled), remaining balance, effective date range, and indicators for rounding/proration/carryover rules applied And rolled-up totals across all engagements/packages are provided And all figures reconcile to the underlying consumption ledger and bundle balances for the same timestamp
Run-out Forecasting
"As a client contact, I want to see a projected run-out date based on current usage and bookings so that I can plan top-ups before work halts."
Description

Projects the expected allowance depletion date using current balance, booked sessions cadence, historical burn rate, and upcoming known commitments. Handles different granularity (hours vs. sessions), sensitivity to cancellations, and optional inclusion of tentative bookings. Displays a projected run-out date with a simple confidence indicator and scenario hints (e.g., “+5 hours extends to Nov 12”). Updates automatically as new sessions are scheduled or usage is recorded, enabling proactive top-up conversations.

Acceptance Criteria
Baseline Run-out Calculation (Bookings + Historical Burn)
Given account timezone = America/New_York and forecast generated at 2025-09-22 10:00 local And allowance unit = hours, total allowance = 40h, used = 28h, remaining = 12h And booked sessions: 2h on 2025-09-23, 2h on 2025-09-25, 2h on 2025-09-30 And historical burn rate over last 6 completed weeks = 6h/week When the forecast is computed Then the projected run-out date = 2025-10-07 23:59 local (remaining 6h after 2025-09-30 consumed at 6h/week) And the run-out date is displayed as a date-only value “Oct 7, 2025” in the Client Usage Portal And if cumulative booked usage exceeds remaining before applying historical burn (e.g., remaining = 5h; next sessions = 3h on 2025-09-23 and 3h on 2025-09-25), Then projected run-out date = 2025-09-25 23:59 local And if there are no bookings and historical burn exists, Then run-out date = today + ceil(remaining/historical_daily_rate) days, rounded to 23:59 local And if there are no bookings and <3 weeks of historical data, Then display “Not enough data to forecast” instead of a date
Unit Granularity Support (Hours vs. Sessions)
Given allowance unit = hours, remaining = 2.5h And booked sessions: 1h on 2025-09-22 15:00, 1.5h on 2025-09-24 10:00 When the forecast is computed Then projected run-out date = 2025-09-24 23:59 local And durations are summed to consume remaining hours Given allowance unit = sessions, remaining = 2 sessions And booked sessions: 1 session on 2025-09-22 15:00, 1 session on 2025-09-24 10:00 (session durations ignored) When the forecast is computed Then projected run-out date = 2025-09-24 23:59 local And when unit = sessions, each booked session consumes exactly 1 regardless of duration
Tentative Bookings Inclusion Toggle
Given Include Tentatives toggle default = Off And remaining = 12h; booked firm sessions: 2h on 2025-09-23, 2h on 2025-09-25, 2h on 2025-09-30; historical burn = 6h/week When Include Tentatives = Off Then projected run-out date = 2025-10-07 23:59 local When Include Tentatives = On and there is a tentative 2h session on 2025-09-27 Then projected run-out date = 2025-10-04 23:59 local And the toggle state persists per client and is reflected immediately in the displayed forecast without page reload
Cancellation and Reschedule Sensitivity
Given remaining = 5h; booked firm sessions: 2h on 2025-09-23 10:00, 3h on 2025-09-24 10:00; historical burn = 7h/week (1h/day); forecast generated 2025-09-22 10:00 local When no changes occur Then projected run-out date = 2025-09-24 23:59 local When the 3h session on 2025-09-24 is cancelled at 2025-09-23 09:00 Then the forecast recomputes within 60 seconds And new projected run-out date = 2025-09-26 23:59 local (remaining 3h after 2025-09-23 consumed at 1h/day) When instead of cancellation, the 3h session is rescheduled to 2025-09-25 10:00 Then the forecast recomputes within 60 seconds And new projected run-out date = 2025-09-25 23:59 local
Confidence Indicator Computation and Display
Given last 6 completed weeks of usage (hours): [4,5,5,6,6,6] When computing variance and coefficient of variation (CV = stdev/mean) Then CV ≈ 0.14 ≤ 0.20 and confidence label = High Given last 6 completed weeks of usage (hours): [1,6,0,8,2,7] When computing CV Then CV ≈ 0.78 > 0.50 and confidence label = Low And threshold rules: CV ≤ 0.20 => High; 0.20 < CV ≤ 0.50 => Medium; CV > 0.50 => Low And if fewer than 3 completed weeks of history, confidence label = Low and tooltip text includes “insufficient history” And the confidence indicator is displayed adjacent to the projected run-out date with a tooltip: “Based on last 6 weeks usage variability”
Scenario Hints for Top-up Effects
Given allowance unit = hours; remaining = 12h; firm bookings: 2h on 2025-09-23, 2h on 2025-09-25, 2h on 2025-09-30; historical burn = 6h/week; base run-out = 2025-10-07 When showing hint for +5 hours Then display “+5h extends to Oct 13, 2025” (remaining after bookings = 11h at 6h/week ≈ 13 days after 2025-09-30) Given allowance unit = sessions; remaining = 3 sessions; firm bookings: 1 session on 2025-09-23, 1 session on 2025-09-30; historical cadence ≈ 1 session/week; base run-out = 2025-10-07 When showing hint for +2 sessions Then display “+2 sessions extends to Oct 21, 2025” And hints render for at least the default increments [+2, +5] in the plan’s unit, using account locale date formatting
Auto-Update on New Usage or Scheduling
Given the Client Usage Portal is open for a client with a displayed projected run-out date When a new booking is created that increases consumption before the current run-out date Then the forecast recomputes and updates the displayed run-out date within 60 seconds without full page reload When usage is recorded (e.g., a completed session logs 1.0h) reducing the remaining balance Then the forecast recomputes and updates the displayed run-out date within 60 seconds And an activity entry is available internally with previous_date, new_date, timestamp for audit
Invoice Dashboard & One-click Payment
"As a client contact, I want to view outstanding invoices and pay them in one click so that service continues without payment delays."
Description

Provides clients a consolidated view of invoices (due, upcoming, paid) with statuses, due dates, and summarized line items. Enables one-click payment via integrated gateways (e.g., Stripe) supporting cards and ACH, with automated receipt emailing and reconciliation to SoloPilot’s invoicing. Allows downloading PDF invoices, viewing payment history, and supports partial payments where enabled. Ensures PCI-compliant handling via provider, maintains a consistent, branded experience, and updates balances in real time after payment.

Acceptance Criteria
Consolidated Invoice List with Status and Due Dates
Given a client with invoices across statuses (Upcoming, Due, Overdue, Paid) When they open the Invoice Dashboard Then the table displays columns: Invoice #, Status, Due Date, Amount, Balance, Last Updated And invoices are sorted by Due Date ascending by default And the client can filter by Status and search by Invoice # And each row shows a summary of line items (count and first two descriptions) with a View details action And Outstanding Balance and Overdue Total metrics are displayed and reflect the dataset And initial load completes within 2 seconds at p95 for up to 200 invoices
One-Click Card Payment via Stripe
Given an unpaid invoice with Balance > 0 and card payments enabled via Stripe When the client clicks Pay Now and submits a valid card through provider-hosted fields Then any required 3DS/SCA challenge is handled within the flow And no raw card data is transmitted to or stored by SoloPilot servers And upon authorization/capture success, the invoice status updates to Paid within 10 seconds And a success message and updated balance are shown immediately And an itemized receipt email is sent to billing contacts within 1 minute And the payment surface adopts workspace branding (logo and primary color) without violating PCI isolation And errors (e.g., insufficient funds, authentication failure) display actionable messages without leaving the page
ACH Payment Support with Pending Status
Given an unpaid invoice with Balance > 0 and ACH enabled via the payment provider When the client selects ACH and completes bank authorization via the provider Then a payment record is created with status Pending (ACH) and an estimated settlement date is displayed And the invoice is protected against duplicate payment attempts until the ACH resolves or is canceled And an initiation email is sent to billing contacts within 1 minute And upon provider webhook indicating settlement, the invoice updates to Paid and a receipt email is sent And upon provider webhook indicating failure/return, the invoice reverts to Due with an error notice to the client
Partial Payment Handling
Given an invoice with partial payments enabled and an outstanding balance When the client chooses Partial Payment and enters an amount between 1.00 (in invoice currency) and the outstanding balance Then the UI validates the amount and blocks submission outside the allowed range And the payment is applied for the entered amount and the invoice status becomes Partially Paid And the remaining balance updates immediately and displays on the dashboard And a receipt reflects the partial amount paid and the remaining balance And payment history logs the partial payment with timestamp, method, and provider reference
PDF Invoice Download
Given any invoice visible to the client When the client clicks Download PDF Then a PDF is generated within 2 seconds with filename pattern Invoice_<invoice_number>.pdf And the PDF includes branding (logo/colors), invoice/billing details, line items (qty, rate, amount), taxes, discounts, totals, and balance due And monetary values match the invoice data to two decimal places with the correct currency symbol/ISO code And the PDF contains a pay link or QR code to the invoice payment page And access is restricted to authorized client sessions; direct link access without authentication is denied
Automated Receipt Emailing and Reconciliation
Given a successful payment (card capture or ACH settlement) for an invoice When the provider sends a confirmation (synchronous or webhook) Then SoloPilot creates a payment record with provider ID, method, last4 (when applicable), gross, fees, net, currency, and timestamp And the invoice status and balance update accordingly and appear in Payment History And a receipt email is sent to billing contacts and CC recipients configured on the invoice within 1 minute And duplicate notifications are ignored via idempotency keys while retries are processed safely And all events are written to an immutable audit log visible to workspace admins
Real-Time Balance and Concurrency Updates
Given a client viewing the Invoice Dashboard or an invoice detail page When a payment is completed from this or another session/device Then totals and the affected invoice row update without page refresh within 5 seconds And any Pay Now actions for a fully paid invoice are disabled immediately And if two sessions attempt to pay the same invoice, the second submission is blocked with a clear message and no duplicate charge is created
Embedded Top-up & Scope Change Actions
"As a client contact, I want embedded buttons to request top-ups or approve scope changes so that I can authorize additional work quickly."
Description

Adds prominent, context-aware action buttons (Request Top-up, Approve Scope Change) inside the portal. Top-up creates a draft invoice or checkout link with predefined packages; scope change routes an approval workflow that confirms revised terms and updates allowance upon acceptance. Captures payer/approver identity, timestamps, and optional notes, and syncs approvals to the client record. Integrates with invoicing and the usage engine so approved actions immediately reflect in balances and forecasts.

Acceptance Criteria
Context-Aware Action Buttons Display
Given a portal user with permission to request top-ups and at least one predefined top-up package exists When the user views the Client Usage Portal Then the "Request Top-up" button is visible and enabled Given no predefined top-up packages exist When a portal user views the Client Usage Portal Then the "Request Top-up" button is not displayed Given there is a pending scope change proposal awaiting client approval When an authorized approver views the Client Usage Portal Then the "Approve Scope Change" button is visible and enabled and displays the proposal summary (title and effective date) Given there is no pending scope change proposal When any user views the Client Usage Portal Then the "Approve Scope Change" button is not displayed Given a user without the required permissions views the Client Usage Portal When the page loads Then both action buttons are not displayed
Top-up via Predefined Package — Draft Invoice Path
Given the organization is configured for draft-invoice top-ups and a portal user with permission selects a predefined package When the user submits the Request Top-up form with package X and optional note Then a draft invoice is created with status "Draft", correct client, currency, tax, and line items matching package X, and a unique invoice ID And the draft invoice appears in the invoicing module within 5 seconds And the portal shows a confirmation with the draft invoice reference And no allowance or balance changes are applied until the draft invoice is approved Given the created draft invoice is approved by an authorized internal approver When the approval is recorded Then the client allowance increases by the package units within 10 seconds and projected run-out date is recalculated And the draft invoice status transitions to "Approved" or equivalent per system configuration Given a user double-submits the same top-up within 60 seconds due to network retry When the backend receives duplicate requests with the same idempotency key Then only one draft invoice is created
Top-up via Checkout — Immediate Payment Path
Given the organization is configured for checkout top-ups and a portal user selects a predefined package When the user initiates checkout Then a secure checkout session/link is generated and associated with the client and selected package Given the user completes payment successfully When the payment provider sends confirmation Then a paid invoice is created with status "Paid" and payment transaction ID stored And the client allowance increases by the package units within 10 seconds and projected run-out date is recalculated And the portal displays a success message with the invoice reference Given the user abandons or fails payment When the checkout session expires or returns a failure Then no allowance changes are applied And no paid invoice is created (a pending/failed payment record may be stored) And the portal displays an error message with a retry option
Scope Change Approval Workflow and Allowance Update
Given a pending scope change proposal exists with revised terms and updated allowance When an authorized approver clicks "Approve Scope Change" and confirms after viewing the summarized changes (scope, pricing, effective date) Then the approval is recorded and linked to the proposal And the proposal status updates to "Approved" And the client's allowance and any impacted billing terms update accordingly within 10 seconds And the usage engine recalculates balances and forecasts Given an authorized approver rejects the proposal and provides an optional note When they submit the rejection Then the proposal status updates to "Declined" And no allowance or billing changes are applied And the rejection note is stored Given multiple scope change proposals are pending When one is approved Then only the approved proposal's changes are applied and other proposals remain unchanged
Audit Trail Capture and Sync to Client Record
Given a user submits a top-up request or approves/declines a scope change When the action is processed Then the system captures and stores the actor's identity (user ID and email), role, timestamp (UTC), action type, related entity IDs (invoice/proposal), and optional note (0–500 characters) And the record is appended to the client's activity log and is retrievable via the client record and API Given an action note exceeds 500 characters When the user submits Then the system prevents submission and displays a validation error without creating or changing records
Real-time Balance and Forecast Refresh
Given a top-up or scope change has been approved and applied When the user remains on the Client Usage Portal Then the displayed allowance, remaining balance, and projected run-out date update to match the usage engine within 10 seconds without requiring a manual page refresh And values shown in the portal match the usage engine values to the smallest displayed unit (e.g., hours to 0.1) Given two approvals are applied within 30 seconds of each other When the portal refreshes data Then the final displayed balance reflects the cumulative effect of both approvals
Authorization and Access Control for Actions
Given a portal session belongs to a user with the "canRequestTopUp" permission When the user views the portal Then the "Request Top-up" button is available Given a portal session belongs to a user with the "canApproveScopeChange" permission When the user views the portal and a scope change is pending Then the "Approve Scope Change" button is available Given a portal session is unauthenticated or lacks required permissions When action endpoints are invoked (top-up request or scope approval) Then the system responds with HTTP 401/403 and does not create invoices, proposals, or change balances And no partial records are committed
Threshold Alerts & Notifications
"As a client contact, I want to receive alerts when usage crosses thresholds or the run-out date is near so that I can act in time."
Description

Configurable alerts notify stakeholders when usage crosses thresholds (e.g., 50%, 75%, 90%), when projected run-out is within a set window, and when invoices are approaching due dates. Supports email and in-portal banners initially, with quiet hours and per-contact preferences. Links in notifications deep-link to the relevant portal section (meter, forecast, payment). Alerts are deduplicated to avoid noise and logged for auditability. Integrates with SoloPilot’s notification framework and respects client timezone settings.

Acceptance Criteria
Usage Threshold Crossed Notification Delivery
Given a client account has usage thresholds configured (e.g., 50%, 75%, 90%) and at least one contact is subscribed to notifications And usage was previously below a configured threshold When recorded usage updates transition to equal or above that threshold Then the system dispatches notifications via the contact’s enabled channels (email and/or in-portal banner) within 2 minutes And the notification includes: threshold crossed, current usage percentage, service period, and a deep link to the Usage Meter section And timestamps and scheduling respect the client’s timezone And the event is recorded with event type, threshold value, current percentage, recipient(s), channel(s), and delivery status
Projected Run-Out Alert Window
Given a client has a projected run-out alert window configured (e.g., N days) and at least one contact is subscribed to run-out alerts And the last computed run-out date was outside the window When the forecast engine calculates a projected run-out date that is within the configured window Then the system sends notifications per contact/channel preferences within 10 minutes And the notification includes: projected run-out date, remaining units/hours, and a deep link to the Forecast section And subsequent recalculations within the cooldown window do not trigger duplicates unless severity increases (e.g., enters a tighter window)
Invoice Due Date Approaching Notification
Given an unpaid invoice has a due date within the configured reminder window (e.g., N days) and a contact is subscribed to invoice reminders When the reminder evaluation runs aligned to the client’s timezone Then a notification is sent including invoice number, amount due, due date, and a deep link to the Payment section And no reminders are sent after the invoice is paid, voided, or written off And duplicate reminders are suppressed within the cooldown window
Per-Contact Notification Preferences Enforcement
Given multiple contacts under a client account with per-contact channel and alert-type preferences When any alert (threshold, run-out, invoice) is triggered Then only contacts opted in to that alert type receive notifications on their enabled channels And contacts who have opted out or unsubscribed do not receive notifications And any preference change takes effect for the next alert without requiring admin intervention
Quiet Hours Suppression and Deferral
Given quiet hours are configured for a client and/or contact in their local timezone When an alert triggers during quiet hours Then outbound email delivery is suppressed and queued for delivery at the end of quiet hours And in-portal banners are created immediately but do not generate an email during quiet hours And only a single deduplicated email is sent at quiet-hours end representing the latest state for that alert type
Deep Links Route to Correct Portal Section
Given a notification is generated for a usage threshold, projected run-out, or invoice reminder When the recipient clicks the link in the email or banner Then they arrive at the correct portal section (Usage Meter, Forecast, or Payment) with relevant context loaded And if not authenticated, they are redirected to login and then returned to the intended section And links preserve any tracking parameters supported by the notification framework
Framework Integration, Deduplication, and Audit Logging
Given SoloPilot’s notification framework is available When an alert event is emitted (threshold crossed, run-out window entered, invoice due) Then the system uses the framework for template rendering, channel routing, retries, and delivery reporting And deduplication is enforced per client/contact/channel/alert-type within a configurable cooldown window And every send or suppression is logged with correlation ID, event type, recipient(s), channel(s), timestamp in the client’s timezone, link target, and outcome (sent, suppressed, failed, retried) And audit entries are visible to authorized users for compliance and troubleshooting
Audit & Activity Log
"As a consultant, I want a complete audit trail of portal access and approvals so that I can maintain compliance and resolve disputes."
Description

Maintains a tamper-evident activity log of portal access, approvals, payments initiated, top-up requests, and settings changes. Each entry records who acted, when, from which IP/device, and what changed, with export capability for compliance or dispute resolution. Access to logs is permissioned; sensitive details are redacted as needed. Integrates with SoloPilot’s global audit infrastructure and ties events to invoices, sessions, and client records for full traceability across the workflow.

Acceptance Criteria
Log Entry Creation and Content Completeness
Given an authenticated actor performs any of the following in the Client Usage Portal: portal sign-in/access, usage view, approval of a session/invoice, payment initiation, top-up request submission, or settings change When the action is successfully committed Then a single audit event is appended within 2 seconds containing: event_id (UUIDv4), actor_id, actor_role (user/client/system), actor_display_name, timestamp_utc (ISO 8601, ms precision), source_ip (IPv4/IPv6), device_fingerprint_hash, event_type, entity_type, entity_id, changed_fields summary (before/after where applicable), correlation_id, and linkage to related invoice/session/client record And the audit store is append-only; updates are rejected and deletions are disallowed And the event is propagated to SoloPilot’s global audit infrastructure with the same event_id and cryptographic event_hash And each new event includes previous_hash to form a verifiable hash chain And timestamps are recorded in UTC and are monotonic per actor session
Permissioned Access and Redaction Controls
Given roles: Workspace Owner, Staff (with View Audit Log permission), Client Contact, External Auditor (time-limited link), and Unauthorized User When requesting audit records for a tenant/client Then only Owner, permitted Staff, and valid External Auditors receive 200; Client Contact receives only events scoped to their organization and contracts; Unauthorized receives 403 with no data leakage And viewing audit logs is itself logged as event_type=audit_read with actor and scope And sensitive values are redacted per role: payment tokens masked to last 4; notes marked sensitive hidden; source_ip truncated (/24 IPv4, /64 IPv6) for Client Contact and External Auditor; device_fingerprint shown only as stable hash; secret keys never displayed to any role And redaction is applied consistently in UI, API, and exports
Search, Filter, and Traceability in UI
Given an authorized user opens the Audit & Activity Log UI When they filter by date range (UTC), actor, event_type, entity_type, entity_id, and source_ip Then the first page of results returns within 800 ms at p95 with default page size 50 and options for 25/50/100 And results are sorted by timestamp desc by default with toggle to asc And each row displays timestamp, actor, event_type, entity_type/id, and a concise change summary And clicking an event opens details with links to the related invoice/session/client record, preserving filters on back navigation And if no records match, the UI shows a clear empty state and offers to reset filters
Export for Compliance and Dispute Resolution
Given an authorized user selects Export When they choose CSV or JSON, apply filters, and confirm Then the system generates a role-redacted export for up to 100,000 events within 60 seconds; larger exports run asynchronously and complete within 5 minutes And each export includes file metadata (generated_at_utc, tenant_id, filter_summary, record_count, schema_version) and a SHA-256 checksum file And each record includes event_id, timestamp_utc, actor, event_type, entity linkage, change summary, source_ip/device (redacted per role), and hash-chain fields (index, previous_hash, event_hash) And the download link is a signed URL that expires after 24 hours and is invalidated on revocation And export creation and download are logged as audit events
Tamper-Evidence and Integrity Verification
Given the audit store is append-only with a hash chain across events When a verification job or API call is executed for a specified time range or sequence window Then the system validates continuity of previous_hash->event_hash for all events in scope and returns Pass with last_verified_index and timestamp, or Fail with the first invalid index and reason And any integrity failure triggers a Sev-1 alert to ops and surfaces an integrity warning banner in the Audit UI And the Audit UI displays an Integrity Verified badge showing last verification time when the latest verification passed within the last 24 hours
Resilience, Performance Impact, and Delivery Guarantees
Given a portal event occurs under normal or degraded audit infrastructure conditions When the application attempts to record the audit event Then end-user action latency attributable to audit logging is ≤200 ms at p95 and never blocks completion; if the audit backend is unavailable, the event is queued locally and retried with exponential backoff for up to 24 hours And deduplication by event_id ensures at-least-once delivery without duplicates And persistent failure after 24 hours raises an ops alert and marks system status as Degraded in the admin dashboard And upon recovery, queued events are written in original occurrence order and pass integrity verification

Proration Wizard

Handles partial cycles automatically when a retainer starts, pauses, or changes scope mid-month. Prorates allowances and invoices accurately, logs the adjustment for auditability, and communicates the change to clients—so onboarding and scope shifts are smooth and error-free.

Requirements

Effective-Dated Proration Engine
"As an admin of a solo practice, I want proration to be calculated automatically when a retainer changes mid-month so that allowances and charges are accurate without manual spreadsheets."
Description

Implements core logic to calculate accurate proration for retainers when they start, pause, resume, or change scope mid-cycle. Allocates both monetary fees and allowances (hours/sessions) proportionally based on configurable proration basis (calendar days vs. remaining cycle), time zone, and cycle anchor. Supports backdated and future-dated changes, leap years, and DST transitions; respects carryover rules and minimum billable increments; and applies configurable rounding modes. Accepts inputs including current/new plan, effective date, price, allowance, usage-to-date, and paused state, and outputs allowance adjustments and credit/charge deltas with a deterministic calculation breakdown. Integrates with scheduling to prevent over-allocation and with invoicing to pass computed deltas downstream.

Acceptance Criteria
Mid-Cycle Start — Calendar-Day Basis
Given a monthly retainer cycle anchored on the 1st at 00:00 in America/New_York and proration basis = calendar-days with monetary rounding = half-up(2 decimals) and allowance rounding = half-up to nearest 0.25 hours and minimum billable increment = 0.25 hours And current plan fee = $0/month and allowance = 0 hours And new plan effective date = 2025-09-16T00:00:00-04:00 with price = $1200/month and allowance = 24 hours When the proration engine runs Then it computes numeratorDays = 15 and denominatorDays = 30 and prorationFactor = 0.5 And outputs chargeDelta = +600.00 USD and allowanceDelta = +12.00 hours And includes a deterministic breakdown with cycleAnchor = 2025-09-01T00:00:00-04:00, effectiveAt = 2025-09-16T00:00:00-04:00, basis = "calendar-days", rounding = {money:"half-up-2dp", allowance:"half-up-0.25h"}
Backdated Pause and Resume — Net Credit for Paused Window
Given a monthly cycle anchored on 2025-10-01T00:00:00-04:00 in America/New_York with proration basis = calendar-days and rounding = money half-up(2dp), allowance half-up to 0.25h And plan fee = $2000/month and allowance = 40.00 hours and usageToDate at pauseEffective = 10.00 hours And a backdated pause effectiveAt = 2025-10-10T00:00:00-04:00 and a resume effectiveAt = 2025-10-25T00:00:00-04:00 When the proration engine runs Then it treats [2025-10-10 .. 2025-10-24] inclusive as paused days with numeratorDays = 15 and denominatorDays = 31 and prorationFactor ≈ 0.483871 And outputs chargeDelta = -967.74 USD and allowanceDelta = -19.25 hours And ensures adjustedAllowance >= usageToDate and includes a breakdown segment for the paused window with explicit date bounds
Scope Increase Mid-Cycle — Remaining-Cycle Basis Delta
Given a monthly cycle anchored on 2025-09-01T00:00:00-04:00 with proration basis = remaining-cycle and rounding = money half-up(2dp), allowance half-up to 0.25h And old plan = $1000/month and 20.00 hours; new plan = $1600/month and 32.00 hours; change effectiveAt = 2025-09-20T00:00:00-04:00 When the proration engine runs Then it computes numeratorDays = 11 and denominatorDays = 30 and prorationFactor = 0.3666667 And outputs chargeDelta = +220.00 USD (from $600 × 11/30) and allowanceDelta = +4.50 hours (from 12.00h × 11/30 rounded half-up to 0.25h) And includes a breakdown referencing basis = "remaining-cycle" with numeratorDays and denominatorDays
Leap Year February — Calendar Basis Correct Denominator
Given a monthly cycle anchored on 2024-02-01T00:00:00-05:00 in America/New_York with proration basis = calendar-days and rounding = money half-up(2dp), sessions rounded to whole units And plan fee = $290/month and allowance = 29 sessions and start effectiveAt = 2024-02-15T00:00:00-05:00 When the proration engine runs Then denominatorDays = 29 and numeratorDays = 15 and prorationFactor = 15/29 And outputs chargeDelta = +150.00 USD and allowanceDelta = +15 sessions And the breakdown flags leapYear = true
DST Transition — Local Time Anchor Without Hour-Weighting
Given a monthly cycle anchored on 2025-11-01T09:00:00-07:00 America/Los_Angeles with proration basis = calendar-days (civil days) and rounding = money half-up(2dp), allowance half-up to 0.25h And plan fee = $900/month and allowance = 18.00 hours and start effectiveAt = 2025-11-15T09:00:00-08:00 When the proration engine runs Then numeratorDays = 16 and denominatorDays = 30 regardless of DST hour shifts and prorationFactor = 0.5333333 And outputs chargeDelta = +480.00 USD and allowanceDelta = +9.50 hours And breakdown timestamps are stored and displayed in the configured time zone with correct UTC offsets
Deterministic Outputs and Idempotent Hash
Given any fixed proration input payload and rounding configuration (e.g., the scope increase example) and the same locale/time-zone settings When the engine is invoked twice with bit-identical inputs Then chargeDelta, allowanceDelta, and the ordered calculationBreakdown are exactly equal across invocations And a calculationHash (e.g., SHA-256 over normalized breakdown) is identical across invocations and changes if and only if any input changes
Downstream Integration — Invoice Delta and Scheduling Guardrails
Given a scope decrease mid-cycle that computes remainingAllowance = 1.50 hours and a credit chargeDelta = -75.00 USD with a populated breakdown When the engine publishes results Then an invoice adjustment line is created with type = credit, amount = 75.00 USD, and memo including effective date, numerator/denominator, basis, and rounding And any attempt to schedule a new 2.00-hour session within the current cycle is rejected with error code = SCHEDULE_OVER_ALLOCATION and the UI displays remainingAllowance = 1.50 hours And existing future bookings that exceed the adjusted allowance are flagged with status = AT_RISK in the scheduling dashboard
Automated Adjustment Invoicing & Credits
"As a practitioner, I want automatic invoices and credits for proration adjustments so that billing stays correct and I get paid on time."
Description

Generates and posts invoice line items for proration deltas, including positive charges and negative credits, with correct tax, discount, and coupon proration. Supports issuing immediate adjustment invoices or rolling adjustments into the next billing cycle based on workspace settings, consolidates related adjustments, and respects minimum invoice thresholds and currency precision. Syncs with payment gateways for charge/credit capture, updates account balance and aging, and ensures one-click session-to-invoice flows include proration adjustments seamlessly.

Acceptance Criteria
Immediate Adjustment Invoice with Mixed Charges and Credits
Given a workspace with Adjustments setting = "Immediate" And an active retainer experiences a mid-cycle scope increase and a partial pause resulting in both a positive and a negative proration delta within the same billing period And itemized tax rates, percentage discounts, and a recurring coupon are applied to the retainer When the proration job runs Then the system generates and posts a single adjustment invoice within 2 minutes containing separate line items for each proration delta (positive charges and negative credits) And each line item shows prorated quantity or time basis, unit price, discount/coupon application, taxable amount, tax amount, and extended total And the invoice subtotal, discounts, taxes, and grand total equal the computed proration math to the currency minor unit with round half up And an audit log entry is created linking the invoice lines to the source retainer change(s) with before/after allowance snapshots and calculation references
Roll-Forward Consolidation with Minimum Threshold
Given a workspace with Adjustments setting = "Roll Forward" and Consolidate Adjustments = "On" And multiple proration deltas occur during the current cycle across the same client and retainer And the Minimum Standalone Invoice Threshold is set in workspace currency When the next billing cycle invoice is generated Then the system consolidates all open proration deltas into a single adjustment section on that invoice with per-delta line items And if the absolute total of deltas is below the threshold, no standalone invoice is created; the amount is carried forward to the next cycle And if a regular cycle invoice exists for the client, the adjustments are merged onto that invoice; otherwise a single consolidated adjustment invoice is created And consolidation preserves per-delta attribution (date and reason) for audit while producing one payable total
Accurate Proration of Taxes, Discounts, and Coupons
Given a retainer with taxable and tax-exempt items, multi-rate jurisdictional taxes, a percentage discount, and a fixed-amount recurring coupon that applies only to eligible recurring charges And a mid-cycle change modifies quantities and effective dates When proration is calculated Then discounts are applied to the prorated base amounts before taxes, limited by their applicability rules And coupons are prorated by time/quantity and only applied to eligible items; fixed coupons are prorated linearly by remaining cycle fraction; percentage coupons are computed on the prorated base And taxes are computed on the post-discount prorated taxable amounts per jurisdiction rules; tax is reversed for negative credit lines where applicable And the sum of all line-level tax amounts equals the invoice-level tax total to the currency minor unit with no rounding drift
Currency Precision and Rounding Compliance
Given workspace currency and minor unit per ISO 4217 (e.g., 2 for USD/EUR, 0 for JPY, 3 for KWD) And proration produces fractional amounts When invoice line items and totals are computed Then all monetary fields are rounded using round half up to the currency minor unit And line-level rounding is preserved; invoice totals equal the sum of rounded line amounts (not re-rounded from unrounded sums) And any residual sub-minor-unit remainder is tracked and recorded in the audit log to ensure repeatable calculations And exported PDFs, client portal views, and API responses display identical rounded values
Payment Gateway Sync and Balance Update
Given gateway integration is enabled and an adjustment invoice (positive total) or credit memo (negative total) is posted And an idempotency key is generated per invoice/credit for the gateway request When the system attempts charge capture (for positive) or credit/refund (for negative) at the payment gateway Then the gateway transaction succeeds and the external transaction ID is stored and linked to the invoice/credit And the client account balance, available credit, and A/R aging buckets update within 60 seconds of posting And retries on transient failures do not create duplicate charges/credits due to idempotency And failures surface as a recoverable payment state on the invoice/credit with a clear error message and no double-posting in accounting
Session-to-Invoice Flow Includes Proration Adjustments
Given a user triggers one-click Session-to-Invoice for a client with pending proration adjustments And workspace setting determines whether adjustments are Immediate or Roll Forward as of the invoice date When the invoice is generated Then all eligible proration adjustment line items are included exactly once on the invoice according to the setting And session fees and proration adjustments appear as separate sections with correct taxes, discounts, coupons, and totals And if including adjustments would violate the Minimum Standalone Invoice Threshold, the session invoice is generated and adjustments are carried forward or placed on a separate invoice per settings And the resulting invoice passes validation and is ready for payment without manual edits
Immutable Audit Trail & Calculation Snapshot
"As an owner, I want an immutable log of proration calculations so that I can audit changes and resolve client disputes quickly."
Description

Persists an append-only log of every proration event capturing who made the change, when, before/after plan details, all calculation inputs and formulas, intermediate steps, rounding applied, and resulting invoice links. Provides searchable, filterable views and export (CSV/JSON) for compliance and dispute resolution, with role-based access controls to protect sensitive billing data. Ensures each record is tamper-evident and time-stamped for full traceability.

Acceptance Criteria
Append-Only Audit Record on Proration Event
Given a proration event (start, pause, resume, or scope change) is initiated via UI or API When the event is saved Then the system appends a new immutable audit record containing at minimum: event_id (UUID), event_type, actor_id, actor_role, client_id, subscription_id, before_plan (name, allowance, rate, billing_cycle), after_plan (name, allowance, rate, billing_cycle), effective_from and effective_to, calculation_inputs (remaining_days, units, rates, discounts, taxes), explicit formulas used, intermediate calculation steps, rounding rule and rounded values, resulting charge/credit amounts, linked invoice_id(s), request_id/correlation_id, source (UI/API), server_timestamp (RFC3339 with timezone), and a record_signature And no existing audit record is altered or deleted And the record links bi-directionally to any resulting invoice(s) by ID
Tamper-Evident Hash Chain and Non-Destructive Corrections
Given the audit log contains prior records When a new audit record is appended Then the record_signature is a deterministic hash of the record payload plus the previous record's signature, forming a verifiable chain And any attempt to edit or delete an existing record is blocked and returns 403/READ-ONLY in API/UI And corrections occur only via a new correction event that references original_event_id and reason And an integrity check endpoint/process can verify the full chain and reports any breakage with the first failing event_id and timestamp
Search and Filter Across Audit Fields (UI and API)
Given at least 50,000 audit records exist When a user queries by any combination of: date range, client_id, subscription_id, actor_id, event_type, invoice_id, amount range, and has_text (free text over formulas/inputs) Then the first page (≤50 results) returns within ≤2 seconds with accurate total_count and stable sort by server_timestamp desc And results are paginated (cursor or page/size) and consistent across UI and API And selecting a result opens the full record, including calculation snapshot and invoice links
CSV/JSON Export with RBAC-Aware Masking
Given a user has permission to export audit records When the user exports the current filtered result set as CSV or JSON Then the export contains all visible fields for that user with sensitive fields masked per RBAC policy And the file is UTF-8 encoded, uses RFC3339 timestamps, preserves numeric precision, and includes a schema header for CSV And exports up to 100,000 records complete within ≤60 seconds; larger exports run asynchronously and notify the user when ready And an export event is logged in the audit log with requester, filter summary, format, row_count, and file_id
Role-Based Access Control for Sensitive Billing Data
Given system roles Admin, Billing, Practitioner, and Auditor exist When a user views or queries audit records Then permissions are enforced as: Admin (full access), Billing (full financial fields), Practitioner (no access to rates/taxes/discounts; masked values shown), Auditor (read-only full access) And unauthorized access attempts return 403 and are logged with user_id, role, endpoint, and timestamp And RBAC is enforced consistently in UI and API, including exports and deep links
Reproducible Calculation Snapshot Matches Invoice
Given an audit record with calculation_inputs, formulas, intermediate steps, and rounding rules exists When the calculation is re-run from the stored snapshot Then unrounded and rounded results match the amounts stored in the audit record And summed totals and tax breakdowns match the linked invoice line items exactly And any mismatch triggers a validation error flagged on the record with validation_status = failed and details
Canonical Timestamping and Ordering for Traceability
Given audit records are created across timezones and sources When records are listed or exported Then each record includes both server_timestamp (RFC3339 with timezone) and actor_local_timestamp with actor_timezone And default ordering is strictly by server_timestamp desc with deterministic tie-breaker on event_id And displayed times respect the viewer's locale settings without altering stored values
Client Notification & Portal Summary
"As a client, I want clear notifications about how my retainer change affects my bill and remaining sessions so that I understand charges and can approve them."
Description

Delivers client-facing notifications when a retainer starts, pauses, resumes, or changes scope, summarizing allowance changes, credits/charges, effective date, next billing date, and links to invoices and detailed breakdowns. Provides email and in-portal notifications with customizable templates, localization, branding, and compliance with consent/preferences. Tracks delivery status, retries failed sends, and stores message history linked to the proration event for transparency.

Acceptance Criteria
Retainer Start Notification Delivered
Given a retainer is started mid-cycle and a proration event is created When the Proration Wizard finalizes the event Then the system generates an email and an in-portal notification using the client's preferred locale and workspace branding And the notification includes: previous allowance, new allowance, prorated allowance for the remainder, credit/charge amount with currency code, effective date, next billing date, and links to the generated invoice (if any) and detailed proration breakdown And the message is associated with the proration event ID and stored in message history And only one notification per channel is sent per proration event (idempotent) And delivery status is initialized to queued and updated as events occur
Mid-Cycle Scope Change Proration Notice
Given a scope change is applied mid-cycle, resulting in allowance and/or rate changes When the proration is computed Then the client notification summarizes deltas (previous vs new allowance/rate), prorated credit/charge, effective date (with timezone), and updated next billing date And the invoice link points to the adjustment invoice or the upcoming invoice section reflecting the proration And the detailed breakdown link opens a client-accessible page whose line items match the internal proration ledger And the subject and body are localized to the client's locale and include the workspace name
Pause and Resume Notifications with Next Billing Date Update
Given a retainer is paused mid-cycle When the Proration Wizard calculates remaining credit and suspends allowance Then a notification is sent per allowed channels indicating pause status, effective date/time, remaining credit amount, and the next billing behavior/date Given a paused retainer is resumed mid-cycle When the resume takes effect Then a notification is sent summarizing restored allowance (prorated if applicable), any applied credits, effective date, and the next billing date And both messages are stored and linked to their respective proration event IDs
Consent, Channel Preferences, and Compliance
Given a client's communication preferences and consent records are stored When a proration event triggers a notification Then notifications are delivered only via channels the client has explicitly allowed for transactional messages And if email is disallowed, only the in-portal notification is created And all emails include required legal details (company name, physical address) and a manage-preferences link; no unsubscribe link is shown if classified as transactional And the system stores a consent snapshot (policy version, timestamp, source) on the message record And notifications are suppressed for addresses with hard bounces or complaints
Delivery Tracking and Retry Mechanism
Given an email notification is queued When delivery fails with a transient error Then the system retries with exponential backoff up to 4 attempts within 24 hours and records each attempt When delivery fails with a permanent error (hard bounce/invalid address) Then status is set to failed without retries and an internal alert is raised to workspace admins When delivery succeeds Then status progresses queued -> sent -> delivered, and open events set status to opened when available And manual resend from the admin UI creates a new send attempt linked to the same proration event And delivery status is visible in the admin message log and client portal (client-visible statuses limited to sent/delivered)
Localization, Branding, and Template Customization
Given workspace templates for proration notifications contain required placeholders When a notification is generated Then all placeholders resolve or fall back to safe defaults; unresolved required placeholders block send with a validation error recorded And dates/times display in the client's timezone; currency amounts use the workspace currency with locale-specific formatting And branding (logo, colors, from-name) is consistently applied to email and portal notifications And admins can preview localized templates for a selected client/event with sample data before sending And if a locale-specific template is missing, the system falls back to the workspace default locale
Client Portal Summary & History Linked to Proration Event
Given a client user is authenticated in the portal When they view the Billing or Notifications section Then a proration summary card shows the latest retainer start/pause/resume/scope change with allowance change, credit/charge, effective date, next billing date, and links to invoice and detailed breakdown And a chronological history of proration-related messages for the last 24 months is accessible with timestamps and delivery status And only users belonging to the affected client account can view the messages; unauthorized access returns 403 And the portal content respects the client's locale and timezone settings
Proration Preview & What-If Simulator
"As an admin, I want to preview proration outcomes before applying changes so that I can choose the right effective date and avoid billing errors."
Description

Enables admins to preview the impact of proposed changes before committing, showing side-by-side differences in allowances, immediate charges/credits, tax, and next-cycle billing. Allows adjusting effective dates, choosing bill-now vs. next-cycle options, and simulating scenarios (e.g., mid-month pause with prior usage). Validates conflicts (overlapping changes, exceeded usage vs. prorated allowance), surfaces warnings, and applies approved changes in a single atomic action that creates events, invoices, and notifications.

Acceptance Criteria
Side-by-Side Preview: Mid-Month Upgrade (Bill Now vs Next Cycle)
Given an active monthly retainer with a defined tax jurisdiction and current cycle in progress And the admin opens the Proration Preview & What-If Simulator When the admin selects an upgraded plan and sets an effective date within the current cycle And chooses Bill Now Then the preview renders within 2 seconds a side-by-side Current vs Proposed view showing: remaining/prorated allowance, immediate charges/credits with tax breakdown, and next-cycle billing totals And all monetary values are rounded to the currency minor unit and line-item sums equal totals And tax is calculated per the client’s tax rules and displayed as separate lines When the admin switches to Next Cycle Then immediate charges/credits display as 0 and next-cycle billing reflects the upgraded plan and allowance
Usage Validation on Mid-Month Pause with Prior Usage
Given an active retainer with tracked usage that exceeds the prorated allowance up to a selected pause date When the admin simulates a pause effective mid-cycle Then the simulator surfaces a warning indicating exceeded usage versus prorated allowance And calculates overage charges according to pricing rules and includes applicable tax And shows net immediate charge/credit and $0 next-cycle billing while paused And prevents Apply if business policy forbids pausing with unpaid overage; otherwise allows Apply and includes the overage on the generated invoice
Effective Date Controls and Timezone Consistency
Given the client’s billing timezone is set When the admin edits the effective date and time for the proposed change Then all proration calculations use the client’s billing timezone, with cycle boundaries at 00:00–23:59 local time And disallowed dates (before service start, beyond allowed backdating window of 30 days, or outside permitted scheduling limits) are blocked with inline validation messages And changing the date/time re-computes proration, taxes, and allowances within 500 ms and updates the preview And the effective date/time is displayed in both client timezone and admin’s local timezone to avoid ambiguity
Conflict Detection for Overlapping Changes
Given there are pending scheduled changes for the same retainer When the admin proposes a change whose effective window overlaps an existing pending change Then the simulator disables Apply and shows an error listing the conflicting change type, effective window, and reference ID And provides a Resolve action to adjust or cancel the conflicting change And once the conflict is resolved, Apply becomes enabled without page refresh and the preview reflects the latest valid state
Atomic Apply with Idempotency and Audit Trail
Given a simulation with no validation errors and an Apply action initiated by an authenticated admin When the admin clicks Apply Then the system performs an atomic transaction that either fully commits or fully rolls back And on success it creates plan-change and proration adjustment events, generates the correct invoice or credit memo with line items (usage, proration, tax), sends client and internal notifications, and writes an audit log with prior vs new values, effective date/time, user, timestamp, and idempotency key And on failure no partial changes are persisted and an error with a correlation ID is shown And retries with the same idempotency key do not create duplicate events or charges And 95th percentile end-to-end processing time is ≤ 5 seconds
Notification Content and Delivery
Given a proration change has been applied successfully When system notifications are sent Then the client email and in-app message include: old vs new allowances, effective date/time, immediate charges/credits with tax, next-cycle billing, and links to the invoice/credit memo and change log And content is localized to the client’s locale and currency formatting And delivery occurs within 2 minutes of Apply And the admin receives a confirmation notification containing the same financial and scheduling details
Public API & Webhooks for Proration Events
"As a developer integrating SoloPilot, I want APIs and webhooks for proration events so that my accounting and CRM systems stay synchronized."
Description

Exposes endpoints to preview, create, and list proration events and to attach them to plan changes, with idempotency keys, pagination, and tenant scoping. Emits webhooks when proration events are created, updated, or invoiced, enabling external accounting and CRM systems to stay synchronized. Provides signed webhook delivery, retry/backoff, and event payloads that include calculation snapshots and related invoice references.

Acceptance Criteria
API Preview: Mid-cycle scope change returns accurate snapshot (no side effects)
Given an authenticated API client scoped to Tenant A with a monthly retainer cycle starting 2025-09-01T00:00:00-04:00 in USD And the current plan allowance is 10 hours per month with a defined unit_price When the client POSTs to /v1/proration-events/preview with change_type=scope_increase, new_allowance=20, and effective_at=2025-09-16T00:00:00-04:00 Then the API responds 200 OK with a calculation_snapshot including original_allowance, new_allowance, unit_price, proration_ratio, proration_amount, currency, effective_at, cycle_start_at, cycle_end_at, and timezone And calculation_snapshot.proration_ratio equals 0.5 and proration_amount equals (new_allowance - original_allowance) * unit_price * proration_ratio rounded to 2 decimals And no proration_event is persisted and no webhook is emitted And the response includes headers Request-Id and Content-Type=application/json and does not require Idempotency-Key
Create Proration Event: Idempotency, tenant scoping, and audit log
Given an authenticated API client scoped to Tenant A and a pending plan change effective_at=2025-09-20T00:00:00-04:00 When the client POSTs to /v1/proration-events with Idempotency-Key=abc123 and body {plan_id=P1, change_type=scope_increase, new_allowance=20, effective_at} Then the API responds 201 Created with proration_event fields id, tenant_id=A, plan_id=P1, status=created, calculation_snapshot, created_at, and audit_log_id And a subsequent identical POST within 24 hours with the same Idempotency-Key returns 200 OK with the same proration_event.id and header Idempotent-Replayed=true And a POST with the same Idempotency-Key but a different body returns 422 Unprocessable Entity with error.code=idempotency_key_conflict And GET /v1/proration-events/{id} by a client scoped to a different tenant returns 404 Not Found And exactly one webhook of type proration.created is enqueued for the event
List Proration Events: Cursor pagination and filters
Given Tenant A has at least 120 proration events across statuses created, updated, and invoiced When the client GETs /v1/proration-events?limit=50 Then the API responds 200 OK with data length 50, has_more=true, next_cursor present, and items ordered by created_at descending And GET /v1/proration-events?cursor={next_cursor}&limit=50 returns the next 50 unique items with no duplicates or gaps And a third page returns the remaining items with has_more=false And GET /v1/proration-events?status=invoiced returns only events with status invoiced And GET /v1/proration-events?created_at[gte]=2025-09-01T00:00:00Z&created_at[lte]=2025-09-22T23:59:59Z returns only events within that range And requesting limit > 200 returns 400 Bad Request with error.code=limit_too_large
Attach Proration Event to Plan Change and invoice linkage
Given Tenant A has plan P1 and a proration_event E1 with status created and no invoice_id When the client PATCHes /v1/plans/P1 with proration_event_id=E1 Then the API responds 200 OK and plan P1 reflects proration_event_id=E1 And proration_event E1 status transitions to updated and emits a proration.updated webhook And when the next invoice is generated for P1, the invoice includes a line item referencing E1 and a proration.invoiced webhook is emitted And proration_event E1 status transitions to invoiced with invoice_id populated and invoiced_at recorded And attempting to attach E1 to a different plan or tenant returns 404 Not Found And attempting to attach an already invoiced event returns 409 Conflict
Webhooks: Signed delivery, retry/backoff, and at-least-once semantics
Given Tenant A has a configured webhook endpoint https://listener.example/webhooks with secret whsec_123 When a proration event is created, updated, or invoiced Then SoloPilot sends an HTTPS POST within 5 seconds containing JSON fields type (proration.created|proration.updated|proration.invoiced), tenant_id, event_id, version, occurred_at, calculation_snapshot, and invoice_id (nullable) And the request includes headers Content-Type=application/json, X-SoloPilot-Delivery-Id (UUID), and X-SoloPilot-Signature with t={unix_ts}, v1={HMAC-SHA256 of raw body using whsec_123} And non-2xx responses trigger retries at approximately 1m, 5m, 15m, 60m, and 6h (max 5 attempts) with exponential backoff and 10% jitter, preserving the same X-SoloPilot-Delivery-Id And deliveries older than 24 hours are no longer retried and are marked failed with final_attempt_at recorded And duplicate deliveries may occur; SoloPilot preserves the same Delivery-Id across retries to support receiver deduplication
Event Payload: Calculation snapshot and invoice references versioned
Given a consumer receives a proration webhook or fetches GET /v1/proration-events/{id} When the consumer inspects the event payload Then the payload contains version=2025-09-22, event_id, tenant_id, plan_id, change_type, effective_at, cycle_start_at, cycle_end_at, currency, unit_price, original_allowance, new_allowance, proration_ratio, proration_amount, and audit_log_reference And if the event is invoiced, invoice_id and invoice_line_item_ids are populated; otherwise those fields are null or absent per schema And all timestamps are ISO 8601 with timezone offset and all monetary amounts are strings with 2 decimal places And payload fields are stable; breaking changes are introduced only by incrementing version and documented accordingly

Contract Router

Auto-assigns sessions, notes, and invoices to the correct retainer based on project, service, tags, or invitee—prompting for confirmation when rules conflict. Prevents misbilling across multiple concurrent retainers and preserves accurate usage and overage calculations without manual sorting.

Requirements

Rule Builder & Management UI
"As a solo practitioner managing multiple retainers, I want to create and prioritize routing rules based on project, service, tags, or invitee so that items automatically attach to the correct retainer without manual sorting."
Description

Provide a centralized interface to create, prioritize, and manage routing rules that map sessions, client notes, and invoices to the correct retainer based on attributes such as project, service, tags, invitee, client, date range, and custom fields. Support rule weighting/priority, fallbacks when no rule matches, and safe publishing with draft/active versions. Include a test/simulation mode to validate a sample session/note/invoice against current rules before saving. Surface inline guidance and examples to reduce misconfiguration. Integrate within SoloPilot settings and reuse existing data models for projects, services, clients, and retainers. Ensure accessibility, mobile responsiveness, and localization readiness.

Acceptance Criteria
Create Rule with Multiple Match Attributes
Given I am in Settings > Contract Router > Rules When I click "New Rule" Then I can set a rule Name and select a Target Retainer (required) And I can add any combination of match attributes: Project, Service, Tags (multi-select), Invitee, Client, Date Range (start/end), Custom Field (key + value exact match) And all selectors are populated from existing Projects, Services, Clients, Invitees, Tags And the form prevents saving if Name, Target Retainer, or at least one match attribute is missing And invalid references display an error and block save And saving persists the rule and the rule appears in the list with its attributes
Rule Priority and Evaluation Order
Given multiple rules exist When I adjust priorities via drag-and-drop or by editing a numeric Priority field Then the list order and stored priority values update upon save And rule evaluation uses top-to-bottom order (lowest index first) And ties on explicit priority are resolved by list position And reloading the page preserves the saved order
Test/Simulation Runner
Given a draft or active rule set exists When I open Test/Simulation and input a sample object type (Session, Note, Invoice) with attributes Then the system returns the matched rule name, the assigned Retainer, and an evaluation trace showing each rule checked and match outcome And if multiple rules match, the system flags a conflict, prompts me to select the intended rule for preview, and suggests resolving by adjusting priority And if no rule matches, the system indicates "No match" and shows the configured fallback result And running a simulation does not create or modify any records
Fallback Routing Configuration
Given I am configuring rules When I open Fallback settings Then I can choose a Default Retainer or set status to Unassigned as the fallback And publishing is blocked until a fallback is configured And the fallback choice is displayed in the rules list header and used in simulation when no rule matches
Draft, Publish, Versioning, and Rollback
Given I have unsaved changes to rules When I save as Draft Then the changes are stored as a new draft version and do not affect active routing And when I Publish the draft Then the draft becomes Active atomically and a new version entry is created with timestamp, author, and changelog And I can view version history, compare Draft vs Active, and roll back to any prior version And rollback restores the selected version as Active without data loss
Inline Guidance and Misconfiguration Prevention
Given I am creating or editing a rule When I focus any match attribute or priority control Then inline helper text and examples are displayed specific to that control And the UI warns about overlapping date ranges or identical attribute sets before save And tooltips or info links explain how priority and fallback work And form-level validation summarizes all issues at the top and moves focus to the first error on submit
Accessibility, Responsiveness, and Localization Readiness
Given I navigate the Rule Builder UI with a keyboard only Then all interactive elements are reachable in a logical tab order, have visible focus, and are operable without a mouse And form controls include accessible names/labels and error messages are announced by screen readers And color contrast meets WCAG 2.1 AA and ARIA roles and states are present on dialogs, lists, and toasts And the layout adapts for viewports from 320px to desktop without horizontal scroll and key tasks remain usable on mobile And all user-visible strings are externalized for translation and the UI renders correctly with a non-English locale with localized date and number formatting And RTL layouts render correctly when locale is set to a right-to-left language
Real-time Auto-Assignment Engine
"As a busy consultant, I want new sessions, notes, and invoices to be auto-assigned to the correct retainer the moment they are created or updated so that billing stays accurate without extra steps."
Description

Implement an event-driven engine that evaluates routing rules and assigns or updates the retainer on sessions, notes, and invoices at creation and on relevant edits (e.g., project/service changes). Guarantee deterministic rule evaluation with precedence and idempotent operations, persisting the chosen retainer and rationale. Re-evaluate assignments when source attributes change, and queue retries for transient failures. Provide performance targets (<150 ms average evaluation) and resilience (at-least-once processing with deduplication). Integrate with scheduling, notes, and invoicing services via internal events and APIs.

Acceptance Criteria
Auto-assign on creation across services
Given a SessionCreated, NoteCreated, or InvoiceCreated event containing projectId, serviceId, tags[], inviteeId, and accountId When the engine evaluates routing rules Then exactly one retainerId is selected according to rule precedence And the target entity is updated atomically with retainerId and assignmentRationale And an internal event containing the assignment decision is published once with correlationId and idempotencyKey And the assignmentRationale persists ruleId, evaluated attributes, and timestamp And if no rule matches, the entity remains without a retainerId and assignmentRationale.reason = "no-match" is persisted
Re-evaluation on attribute change
Given a session, note, or invoice with an existing retainer assignment And one or more of projectId, serviceId, tags[], or inviteeId is changed via API or UI When the change event is consumed by the engine Then rules are re-evaluated using the updated attributes And if the winning retainer differs from the current assignment, the entity's retainerId and assignmentRationale are updated atomically and a decision event is published And if the winning retainer is the same, no write occurs (no-op) and no duplicate event is published And if evaluation results in a conflict condition, the assignment is not changed and a conflict notification is emitted for confirmation
Deterministic rule precedence
Given multiple routing rules match an entity and have defined precedence values When the engine evaluates the rules Then the rule with the highest precedence is consistently selected as the winner And for identical inputs evaluated multiple times (including retries or redeployments), the selected retainerId is identical across runs And the ordered list of evaluated rules and the winner are recorded in the assignmentRationale for auditability
Conflict detection and confirmation prompt
Given two or more matching rules with equal precedence that resolve to different retainers When the engine evaluates an entity (create or update) Then a conflict is detected and flagged without changing any existing confirmed retainer assignment And a confirmation-required record is created containing candidate retainers, rule details, and correlationId And when a user confirms a selection via the confirmation API Then the engine applies the chosen retainer, updates assignmentRationale with confirmation metadata (userId, timestamp, method), and publishes a confirmation decision event
At-least-once processing with idempotency and deduplication
Given the engine receives duplicate create/update events with the same idempotencyKey or eventId When processing the events Then the assignment outcome is applied exactly once and subsequent duplicates are no-ops And only one decision event exists for that correlationId And a deduplication metric is incremented for skipped duplicates And in case of transient failures (timeouts, 5xx) the engine enqueues retries with exponential backoff up to the configured maxRetries (default 5) And messages exceeding maxRetries are moved to a dead-letter queue with failure context for replay
Performance SLA monitoring
Given production-like load When measuring from event receipt to decision persisted Then the rolling 5-minute average evaluation latency is < 150 ms And latency, throughput, retries, dedup hits, and conflict counts are exported as metrics And distributed traces include correlationId linking inbound event, rule evaluation, and persistence steps
Persistence and audit trail
Given any assignment decision, update, or confirmation When the operation completes Then the entity stores retainerId and assignmentRationale including ruleId, inputs snapshot, decision timestamp, and (if applicable) confirmation metadata And an immutable audit log entry is appended capturing before/after retainerId, evaluator version, and actor (system or user) And read APIs for sessions, notes, and invoices return the current retainerId and assignmentRationale And audit logs can be queried by entityId within 1 second for the most recent 10 entries
Conflict Resolution & Confirmation Prompt
"As a practitioner, I want to be prompted to confirm the retainer when rules conflict so that I can prevent misbilling while maintaining speed."
Description

Detect and handle conflicts where multiple rules match or where a prior assignment no longer satisfies rules. Present a non-blocking prompt showing the recommended retainer, all viable alternatives, and the reason for the recommendation, allowing one-click confirm or override. Support keyboard shortcuts and mobile-optimized interactions. Provide configurable defaults (auto-apply highest-priority rule after X seconds or require confirmation for high-risk cases). Log user decisions for analytics and feed them back to refine rule priorities. Integrate prompts within scheduling, notes, and invoicing UIs and via in-app notifications.

Acceptance Criteria
Multiple Matching Rules in Scheduling Flow
Given a new session is saved and at least two routing rules match the session metadata (project, service, tags, invitee), When routing executes, Then a non-blocking prompt appears within 300 ms containing: the recommended retainer, all viable alternatives, and a reason string for each retainer. Given the prompt is displayed, When the user clicks Confirm on the recommended retainer, Then the session is assigned to the recommended retainer, the prompt closes, and the assignment reflects immediately in the session header. Given the prompt is displayed, When the user selects any alternative retainer and clicks Override, Then the session is assigned to the selected retainer and the prompt closes. Given the prompt is displayed, When the user ignores the prompt and continues editing, Then no actions are blocked and the prompt remains accessible as a floating element anchored to the session form.
Prior Assignment Invalidated on Tag Change
Given a note already assigned to a retainer, and a subsequent change to tags/services causes the assignment to violate current rules, When the note is opened or saved, Then a non-blocking prompt appears indicating the mismatch with a recommended retainer and reasons. Given the mismatch prompt is displayed, When the user clicks Keep Current, Then the assignment remains unchanged and an in-app notification is created to revisit the conflict later. Given the mismatch prompt is displayed, When the user clicks Reassign, Then the note is reassigned to the recommended or selected retainer and usage/overage metrics are recalculated within 1 second. Given a reassignment occurs, When the audit log is queried, Then an entry exists with previous retainer, new retainer, user id, timestamp, and reason.
One-Click Confirm or Override with Shortcuts
Given the prompt is in focus on desktop, When the user presses Enter, Then the recommended retainer is confirmed. Given the prompt is in focus on desktop, When the user navigates alternatives using Arrow keys and presses Space, Then the highlighted alternative is selected and confirmed. Given the prompt is displayed on desktop, When the user presses Esc, Then the prompt dismisses and no assignment changes occur. Given the prompt is displayed on mobile, When the user taps Confirm or Override, Then the action executes; tap targets are at least 44x44 px and respond within 100 ms. Given a screen reader is active, When the prompt opens, Then the title, recommended retainer, reasons, and actions are announced with appropriate roles and labels.
Auto-Apply After X Seconds and High-Risk Confirmation
Given auto-apply is enabled with X seconds in settings, When a conflict prompt opens, Then a visible countdown shows X seconds and the recommended retainer is automatically applied if no user action occurs before the timer elapses. Given a conflict prompt is open and the case meets any high-risk condition (invoice total >= configured threshold, retainer projected to exceed limit within 10%, or client flagged as high-risk), When the timer elapses, Then auto-apply is suppressed and explicit confirmation is required. Given the countdown is running, When the user interacts with the prompt (hover, keypress, tap), Then the timer pauses and resumes only after 1 second of inactivity. Given an auto-apply occurs, When analytics are queried, Then an event exists with autoApply:true, elapsedSeconds, context, and outcome.
Decision Logging and Feedback to Rule Priorities
Given any prompt action occurs (confirm, override, keep current, dismiss, auto-apply), When the action completes, Then a decision log event is sent within 2 seconds including user id, entity type and id, recommended retainer id, selected retainer id, reasons, rule match scores, high-risk flag, and latency. Given network failure during event submission, When retries are attempted, Then events are retried with exponential backoff up to 3 times and queued offline for up to 24 hours. Given a recurring override where the same alternative retainer is chosen over the recommendation >= 5 times within 7 days for the same project/service, When the nightly rules refinement job runs, Then the alternative retainer's rule priority increases and becomes the recommendation for subsequent matches, with a versioned change record visible in admin settings.
Cross-UI Prompt Integration and In-App Notification
Given a conflict arises in scheduling, When the user saves a new session, Then the prompt appears inline in the scheduling modal within 300 ms. Given a conflict arises in the notes editor, When the user opens or saves a note, Then the prompt appears anchored to the note header within 300 ms. Given a conflict arises during invoice creation, When the invoice draft is generated, Then the prompt appears in the invoice sidebar within 300 ms. Given a prompt is dismissed without action, When the notifications panel is opened, Then a notification exists linking back to the originating entity and reopens the prompt in context.
Retainer Usage Ledger Sync & Overage Guardrails
"As a practice owner, I want assignments to update retainer usage and overage calculations automatically so that remaining balances and invoices stay accurate without manual reconciliation."
Description

On assignment and reassignment, atomically adjust the retainer’s usage ledger (time, sessions, or monetary credits), ensuring no double counting and proper proration across billing periods. Support multiple retainer types (time-based, session-based, dollar-based) with configurable rounding and timezone rules. Automatically detect and flag overages, trigger appropriate invoicing behaviors, and display remaining balance in-context. When overrides occur, recalculate usage deltas and update linked invoices to keep totals accurate. Expose a lightweight API for usage reads to other SoloPilot modules.

Acceptance Criteria
Atomic ledger update on session assignment and reassignment
Given a 60-minute session S is unassigned When S is assigned to Retainer R1 Then R1's usage ledger increases by exactly 60 minutes in the current billing period within 1 second And a single immutable usage entry referencing S, periodId, and transactionId is recorded And retries with the same transactionId create no additional usage entries (idempotent) Given S is reassigned from R1 to Retainer R2 When the reassignment is confirmed Then R1 decreases by 60 minutes and R2 increases by 60 minutes atomically in one transaction And no intermediate state is visible to UI or API consumers And linked draft invoices for R1 and R2 reflect the net change within 5 seconds Given two concurrent assignments of S to different retainers occur When the operations race Then exactly one succeeds and the other returns HTTP 409 with no ledger mutation
Cross-period proration using retainer timezone
Given a retainer with monthly periods aligned to timezone T and rounding mode "ceil to 6 minutes" When a 90-minute session spans 23:30–01:00 local time T Then 30 minutes are debited to the ending period and 60 minutes to the new period before rounding And rounding is applied per period independently, resulting in 36 and 60 minutes respectively Given a session occurs across a DST fall-back (ambiguous hour) in T When usage is computed Then minutes are counted once using IANA timezone rules with wall-clock continuity and no negative or duplicated minutes Given a reassignment moves a cross-period session from Retainer R1 to R2 with different period boundaries When the reassignment completes Then prior-period debits in R1 are reversed and reapplied to the corresponding periods in R2 using the same proration and rounding rules And all affected period balances remain non-negative after reversal and re-application
Multi-retainer types with rounding rules
Given a time-based retainer with 15-minute increment and rounding mode "nearest (.5 up)" When a 38-minute single-period session is assigned Then 38 minutes are rounded to 45 minutes and debited Given a time-based retainer with 15-minute increment and rounding mode "floor" When a 14-minute single-period session is assigned Then 0 minutes are debited and the usage entry metadata records roundingMode=floor and increment=15 Given a session-based retainer When any session is assigned Then exactly 1 session unit is debited per session regardless of duration Given a dollar-based retainer When an amount of $125.37 is associated with the session Then exactly 125.37 currency units are debited with precision 2 and currency from the retainer settings
Overage detection and invoicing triggers
Given a retainer with 2 hours remaining and overage rate $150/hour billed immediately When a 6-hour session is assigned Then overage of 4 hours is detected and flagged on the retainer and session And an immediate invoice is created with a line item "Overage 4h @ $150" totaling $600 within 10 seconds And the usage ledger records 6 hours consumed and 4 hours overage Given a retainer policy "block on overage" When an assignment would exceed remaining balance Then the assignment is blocked, the UI shows "Requires approval", and no ledger or invoice mutation occurs Given a retainer policy "allow and bill on next cycle" When overage is detected Then an overage placeholder is added to the next recurring invoice draft with the correct quantity and rate
In-context remaining balance display
Given a user opens a session and selects a candidate retainer When predicted usage is computed Then the UI displays current remaining, post-transaction remaining, and estimated overage (if any) within 500 ms using the retainer timezone Given multiple retainers match routing rules When the user opens the balance tooltip Then a breakdown shows period start/end, used, remaining, overage, timezone, and rounding increment for each candidate Given the assignment completes When the session card reloads Then the displayed remaining balance matches the ledger read API within 1 second (read-after-write)
Override recalculation and invoice sync
Given a session billed on invoice INV-123 from Retainer R1 When an admin overrides assignment to Retainer R2 Then R1 usage is reversed and R2 usage is applied with the same proration and rounding rules And if INV-123 is Draft, its line items are adjusted in place to match the new totals And if INV-123 is Sent-Unpaid, a revision replaces INV-123 with updated totals And if INV-123 is Paid, a credit memo for the reversed amount is issued and a new invoice for the new amount is created And resulting document totals equal the ledger deltas within 5 seconds And an audit log entry records before/after values, actorId, and timestamps Given partial payments exist on INV-123 When the override is applied Then credit memo and new invoice amounts net against existing payments without leaving orphaned balances
Usage read API for SoloPilot modules
Given a valid API token with scope retainer.read When GET /api/v1/retainers/{retainerId}/usage?period=current is called Then respond 200 with JSON containing type, periodStart, periodEnd, used, remaining, overage, timezone, rounding, and updatedAt (ISO-8601) consistent with the ledger And p95 latency is <= 200 ms under nominal load Given the client sends If-None-Match with a current ETag When no changes occurred since last read Then respond 304 Not Modified Given a write just committed for the retainer When the usage endpoint is called within 2 seconds Then the response reflects the latest committed state (read-after-write consistency <= 2s) Given the token lacks access to the retainer When the endpoint is called Then respond 403 without revealing existence Given an invalid period query (e.g., period=2025-13) When the endpoint is called Then respond 400 with a machine-readable validation error
Audit Trail, Undo & Reconciliation
"As a business owner, I want a clear audit trail and reversible actions for retainer assignments so that I can resolve disputes and maintain compliance."
Description

Record an immutable audit trail for every routing decision, including evaluated attributes, matched rules, user overrides, timestamps, and actor IDs. Provide per-item timelines and a one-click undo/reassign action that automatically reverses and reapplies ledger impacts and invoice links. Offer exports (CSV/JSON) and a reconciliation view highlighting conflicts, overrides, and items lacking rules. Enforce data retention and access controls to protect sensitive information while enabling dispute resolution and compliance.

Acceptance Criteria
Immutable Audit Record on Routing Decision
- Given any routing decision (auto or manual) is committed for a session, note, or invoice - Then an audit record is appended containing: item_type, item_id, evaluated_attributes (project, service, tags, invitee), rule_set_version, matched_rule_ids (ordered), outcome_retainer_id, actor_type (system|user|api), actor_id, action_type (route|override), timestamp (UTC ISO-8601 with ms), request_id - And the record includes a SHA-256 checksum and is write-once; no update/delete operation exists via UI or API - And any attempt to modify or delete an audit record returns 403 and is logged as a security event - And the audit write completes within 300 ms P95 at 50 routing events per second sustained
Per-Item Timeline View of Routing Events
- Given a user with Audit:View permission opens an item’s Audit tab - Then the timeline lists all routing-related events in reverse chronological order - And each entry displays: action_type, actor, before/after retainer, matched_rule_ids, invoice_link_ids (if any), timestamp (workspace TZ and UTC), request_id - And entries link to the matched rule definition and actor profile - And the view supports filter by action_type and date range, and search by item_id - And pagination supports at least 500 entries with response time ≤ 800 ms P95
One-Click Undo and Reassign with Ledger and Invoice Reversal
- Given an item has an existing routing decision with ledger and invoice impacts - When the user clicks Undo - Then the system atomically reverses ledger deltas and unlinks associated invoice line items, and appends an audit entry of type undo - And the UI confirms success within 2 seconds P95 - When the user selects Reassign to a different retainer - Then the system reapplies routing, recalculates ledger deltas, and relinks invoice lines to the target retainer; a new audit entry is appended - And if the original invoice is paid, the system issues a credit note and creates/updates the new invoice accordingly, linking all artifacts in audit entries - And the operation is idempotent using a client-supplied idempotency key
Audit Trail Export to CSV and JSON
- Given a user with Audit:Export permission requests an export with filters (date range, item_type, action_type, retainer_id) - Then an async job is queued and a downloadable file is produced within 5 minutes for up to 100,000 records - And CSV output is RFC 4180 compliant with UTF-8 encoding and header row; JSON output is newline-delimited (NDJSON) - And each record includes all required fields plus request_id and workspace_id - And the download URL is access-scoped to the requester, expires after 7 days, and is delivered via in-app notification and email - And a SHA-256 checksum is provided alongside the export to verify integrity
Reconciliation View for Conflicts, Overrides, and Unrouted Items
- Given the user opens the Reconciliation view - Then the view displays three buckets: Conflicts (non-deterministic multi-match), Overrides (manual deviations), Unrouted (no rule matched) - And each bucket shows total count, supports filters by date range and retainer, and allows sorting by timestamp and severity - And clicking an item opens its timeline and provides actions: Resolve (set rule priority), Apply Rule, Assign Retainer, Undo - And bulk actions operate on up to 200 items per request with per-item audit entries created - And counts refresh within 60 seconds of new events and after bulk operations
Data Retention and Access Controls for Audit Data
- Given workspace retention is configured to N days (365 ≤ N ≤ 3650) - Then audit records are retained for at least N days and hard-deleted after N via scheduled jobs, with deletion events logged in a separate immutable log - And role-based access enforces: Owner/Admin (view/export), Billing (view), Auditor (view/export with redactions), Collaborator (no access) - And PII fields (e.g., client name/email) are redacted in exports for Auditor role unless Explicit Access is granted - And all audit data is encrypted at rest (AES-256) and in transit (TLS 1.2+) - And unauthorized access attempts return 403 and are logged with actor_id, IP, and timestamp
Bulk Backfill & Reassignment Tool
"As an operations-focused user, I want to bulk reassign historical items to the right retainers so that I can correct past errors quickly without manual edits."
Description

Enable batch selection of historical sessions, notes, and invoices to re-evaluate against current rules, with a dry-run preview of proposed retainer changes and usage deltas before applying. Support filters (date range, client, project, service, tag), progress tracking, throttling, and retry of failed items. Protect already finalized or paid invoices with configurable safeguards and require confirmation before changes that impact issued billing. Send a summary report upon completion with a link to the reconciliation view.

Acceptance Criteria
Dry-Run Preview Shows Proposed Retainer Changes and Usage Deltas
Given a user selects a set of historical sessions, notes, and invoices via filters and initiates a Dry Run When the system evaluates current Contract Router rules against the selected items Then a preview lists each item with its current retainer and the proposed retainer And each item displays the matched rule identifier(s) and change reason And each item displays usage delta (units/hours) and monetary impact And an aggregate summary shows totals by retainer: proposed adds, removals, net deltas, and monetary impact And items with no proposed change are clearly labeled and may be excluded from apply And the Apply Changes action remains disabled until the preview completes without validation errors
Batch Selection and Filtering of Historical Items
Given a user opens the Bulk Backfill & Reassignment Tool When the user sets filters for date range, client(s), project(s), service(s), and tag(s) Then the system computes the result set server-side and paginates the items And the UI displays counts per type (sessions, notes, invoices) in scope And only items within the selected filters are included in Dry Run and Apply And the user can select all, none, or a subset of items before initiating a Dry Run
Progress Tracking with Throttling and Retry
Given the user clicks Apply Changes on a validated dry-run preview When the batch job starts Then a real-time progress indicator shows total, processed, succeeded, failed, skipped, and ETA And processing respects throttling to avoid rate limits and system overload And transient failures are retried automatically up to the configured maximum attempts with backoff And permanently failed items are listed with error codes and are eligible for manual retry And job state persists so progress can resume after temporary interruption or user reconnect
Safeguards and Confirmation for Finalized or Paid Invoices
Given the selected set includes finalized or paid invoices When safeguard policies are enabled Then invoices blocked by policy are excluded from changes and reported with a clear reason And if policy allows changes with confirmation, the user must review an impact summary and type an explicit confirmation phrase before proceeding And no changes are applied to issued billing without the required confirmation And any allowed changes to finalized or paid invoices record a billing-impact flag in the audit log
Conflict Resolution When Multiple Rules Match
Given an item matches multiple Contract Router rules during re-evaluation When the dry-run preview is generated Then the item is flagged as conflicted and lists the matching rules in precedence order And the user can resolve conflicts by selecting the target retainer per item or applying a global precedence rule And unresolved conflicts are excluded from Apply and counted in the summary with a clear reason
Apply Changes and Persist Audit Trail
Given a batch is applied from a validated preview When retainer assignments are updated Then each changed item records an audit entry with: previous retainer, new retainer, matched rule ID, user ID, timestamp, and batch ID And unchanged and skipped items are logged with reason codes (e.g., No Change, Safeguard, Conflict) And audit entries are accessible from both the item detail and the batch job detail view
Completion Summary Report with Reconciliation Link
Given a batch job completes or is aborted When the job reaches a terminal state Then a summary is delivered to the initiating user via email and in-app notification And the summary includes counts for evaluated, changed, unchanged, skipped (by safeguard), failed, and retried items, plus unresolved conflicts And the summary includes aggregate usage and monetary deltas by retainer And the summary provides links to the batch job detail and the reconciliation view

Underuse Rescue

Detects underutilization early and suggests actions—priority booking slots, friendly nudges, or limited rollover offers—to help clients realize full value before cycle end. Protects retention, reduces churn risk, and converts idle capacity into scheduled work.

Requirements

Utilization Signals & Thresholds
"As a solo practitioner, I want automatic detection of underused packages and retainers so that I can intervene before the cycle ends and clients miss value."
Description

Implements a data model and rules engine to calculate per-client utilization within each billing cycle (session packs, retainers, subscriptions). Continuously aggregates scheduled, completed, cancelled, and no-show sessions from the calendar and session logs to compute remaining credits/time and percentage used. Enables configurable trigger thresholds (e.g., under 60% utilized with less than 25% of cycle remaining) at workspace, service, and client levels. Supports real-time/event-driven updates and daily recomputes, handles time zones, and surfaces trigger events to downstream automations. Provides admin controls to define exclusions (e.g., paused clients) and visibility via client record badges and a risk queue.

Acceptance Criteria
Per-Client Utilization Calculation Across Cycle Types
Given a client has an active session pack, retainer, or subscription with a defined total and billing cycle in the client's billing timezone When sessions with statuses Scheduled, Completed, Canceled, and No-Show exist within the cycle window Then the system aggregates counts and durations by status within that cycle window And remaining_amount = max(0, total_amount - completed_amount) And percent_used = (completed_amount / total_amount) expressed as a percentage with 1-decimal rounding (or 0% if total_amount = 0) And sessions outside the cycle window (by billing timezone) are excluded from the aggregates And mixed services within the same client/cycle use the correct unit (credits or time) per plan type for calculations
Threshold Evaluation with Workspace, Service, and Client Overrides
Given workspace default thresholds and service- and client-level overrides exist When percent_used < utilization_threshold AND cycle_remaining_percent <= remaining_threshold Then an Underuse trigger is created for that client and cycle And only one active trigger exists per client per cycle per threshold rule And precedence is applied as: client override > service override > workspace default And threshold values are validated to be between 0 and 100 inclusive And when percent_used >= utilization_threshold OR cycle_remaining_percent > remaining_threshold, the trigger is resolved automatically
Event-Driven Updates and Daily Recompute SLA
Given an active billing cycle and a defined workspace timezone When a session is created, updated, canceled, or marked no-show affecting utilization within the cycle Then utilization aggregates and threshold evaluations are updated within 60 seconds And a daily recompute runs per workspace respecting the workspace timezone day boundary and completes within 15 minutes And recompute backfills missed changes and corrects any drift so that results match event-driven state And processing is idempotent so repeated events do not create duplicate triggers or double-count usage
Admin Exclusions and Pause Handling
Given an admin marks a client as paused or excluded from Underuse Rescue with effective dates When the exclusion is active Then the client is skipped from threshold evaluation and no Underuse triggers are created And the client does not appear in the risk queue and no underuse badge is shown And utilization metrics remain viewable in the client record details for transparency And when the exclusion ends or the client is unpaused, evaluations resume on the next compute cycle And all exclusion changes are audit-logged with user, timestamp, and reason
Trigger Event Emission for Automations
Given an Underuse trigger is created, updated, or resolved When the trigger state changes Then an event is emitted to the automations bus within 60 seconds And the payload includes: event_type, workspace_id, client_id, cycle_id, service_ids, cycle_start, cycle_end, timezone, total_amount, completed_amount, remaining_amount, percent_used, thresholds_applied, trigger_state, occurred_at And an idempotency_key uniquely identifies (client_id, cycle_id, threshold_id, trigger_state) to prevent duplicate downstream actions And delivery is at-least-once with retries and exponential backoff for transient failures And consumers can correlate events via stable IDs and timestamps
UI Visibility: Client Badge and Risk Queue
Given a client has an active Underuse trigger Then the client record displays a utilization badge showing percent_used and days_remaining in the cycle, and links to utilization details And the risk queue lists all clients with active Underuse triggers and supports filtering by service and owner, search by client name or email, sorting by percent_used ascending then days_remaining ascending, and pagination of 50 per page And each row shows client name, plan type, percent_used, days_remaining, last activity date, and trigger scope (workspace/service/client) And timezone labels reflect the workspace timezone for cycle boundaries And clearing the trigger removes the badge and the client from the risk queue within 60 seconds
Underuse Risk Scoring
"As a solo practitioner, I want a clear risk score with reasons so that I know whom to prioritize and why."
Description

Calculates a risk score and tier (Low/Medium/High) for each active client-cycle using factors such as time-to-cycle-end, unused credits, recent activity, cancellation rate, historical booking cadence, and response latency to outreach. Produces reason codes (e.g., "few sessions scheduled," "infrequent engagement") for transparency. Exposes scores in the client list, client profile, and API, with timestamps and next recompute time. Includes safeguards like minimum data requirements and decay logic to avoid noisy fluctuations.

Acceptance Criteria
Score and Tier Computation
- Given an active client-cycle with valid input factors (time_to_cycle_end, unused_credits, recent_activity, cancellation_rate, booking_cadence, outreach_response_latency), when the risk engine computes, then it outputs a numeric score in the range 0–100 (inclusive) with precision ≤ 0.1 and a tier in {Low, Medium, High} mapped by default thresholds: Low 0–33, Medium 34–66, High 67–100. - Given identical inputs, when the score is recomputed multiple times, then the score is deterministic (variance ≤ 0.1) and the tier remains unchanged. - Monotonicity constraints hold ceteris paribus: when unused_credits increase, or days_to_cycle_end decrease, or recent_activity decreases, or cancellation_rate increases, or booking_cadence slows, or outreach_response_latency increases, then the risk score does not decrease. - The computation completes within 500 ms P95 per client-cycle on production-like data sizes.
Reason Codes and Impact Ordering
- On each compute, return 1–3 reason_codes ordered by absolute impact on the score. - Each reason_code includes: code (snake_case), label (human-readable), impact_direction ∈ {positive, negative}, and contributor_value (string summarizing the driver). - Supported codes include at minimum: few_sessions_scheduled, infrequent_engagement, high_cancellation_rate, slow_response_latency, low_booking_cadence, approaching_cycle_end_with_unused_credits. - For High tier results where unused_credits > 0 and recent_activity is below historical cadence, at least one reason_code references unused_credits or recent_activity. - Absent material input changes, reason_codes remain stable between consecutive recomputes.
UI Surfaces and Timestamp Visibility
- Client List shows for each active client-cycle: risk_tier badge, numeric score, computed_at (ISO 8601 UTC), and a tooltip listing reason_codes (labels), updating within 2 minutes of a recompute without full page reload. - Client Profile includes a Risk panel displaying: score, tier, computed_at, next_recompute_at (both ISO 8601 UTC), and up to 3 reason_codes with labels. - When now > next_recompute_at + 5 minutes, a Stale indicator appears on both Client List and Client Profile. - All risk badges and tooltips meet WCAG AA color contrast and expose accessible names: screen readers announce "Underuse risk: <Tier>, score <value> out of 100".
API Exposure and Contract
- GET /v1/clients/{id}/cycles/{cycle_id}/underuse_risk returns 200 with JSON fields: client_id, cycle_id, score (0–100 or null), tier (Low|Medium|High|Unknown), reason_codes[] (objects with code,label,impact_direction,contributor_value), computed_at (ISO 8601 UTC), next_recompute_at (ISO 8601 UTC), data_quality.minimum_data_met (boolean), data_quality.notes (string|null), version (semver). - GET /v1/underuse_risk?status=active supports filtering by tier in {Low,Medium,High}, min_score, and pagination (page, per_page) and aligns with UI list contents for the same filters. - When data_quality.minimum_data_met=false, score=null and tier=Unknown; response is HTTP 200 and includes reason_codes=['insufficient_data']. - ETag header is returned; If-None-Match is honored to return 304 when unchanged in the last 24 hours.
Minimum Data and Noise Dampening
- Minimum data rule: if completed_sessions < 2 OR (cycle_age_days < 7 AND no_outreach_response_data=true), then data_quality.minimum_data_met=false; output score=null, tier=Unknown, and include reason_codes containing 'insufficient_data'. - Decay logic: absent new qualifying events since last compute, |score_t - score_{t-1}| ≤ 2 points and tier does not change more than one level within any 24-hour window. - Single-event resilience: a single cancellation event within a 24-hour window cannot change tier from Low directly to High. - Response includes decay_parameters.version (string) and smoothing_method (string) for auditability.
Recompute Cadence and Event Triggers
- Scheduled recompute executes at least every 24 hours for all active client-cycles and sets next_recompute_at ≥ computed_at + 24h (unless an earlier event trigger is registered). - Event-driven recompute occurs within 2 minutes after: session booked, session completed, session canceled, outreach sent, outreach reply received; computed_at updates, and next_recompute_at recalculates. - After cycle_end + 14 days, recomputes cease; API returns 404 for inactive cycles; UI no longer displays risk for that cycle. - Concurrency control ensures at-most-once compute per client-cycle: concurrent triggers coalesce, producing a single result with the latest inputs.
Action Recommendation Engine
"As a solo practitioner, I want the system to suggest the best next action for each at-risk client so that I can rescue utilization with minimal effort."
Description

Maps risk tiers and reason codes to recommended interventions—priority slot offers, friendly nudges, or limited rollover credits—using configurable playbooks. Considers practitioner preferences, client channel opt-ins, service type, and cooldown periods. Generates suggested message copy, call-to-action links, and slot selections. Supports auto-run (hands-off) or approve-and-send workflows, with audit logs. Learns from outcomes by feeding booking and response data back into recommendation weighting.

Acceptance Criteria
Playbook Rule Resolution and Intervention Selection
Given a client with risk tier High, reason codes [LowSessionsUsed, UpcomingExpiry], and service type Coaching with an active playbook mapping High + LowSessionsUsed -> PrioritySlots When the engine evaluates rules Then it selects the PrioritySlots intervention for that client-service and records the applied rule ID Given multiple matching rules with defined priority weights When the engine evaluates them Then it chooses the highest-priority rule deterministically and records tie-break metadata Given no matching rule for the client's attributes When evaluation completes Then the engine applies the configured default fallback intervention and records fallback_applied: true Given a playbook is disabled or out of effective date range When evaluation runs Then rules from that playbook are ignored
Preference and Opt-in Compliance
Given the practitioner has disabled SMS and the client is opted in to Email only When generating a recommendation Then only Email-channel interventions are proposed and SMS templates are excluded Given the client has not opted in to any channels When generating a recommendation Then the engine suppresses the recommendation and records suppression_reason: no_opt_in Given a service type requires a specific template variant When the required template is unavailable Then the engine falls back to a generic, allowed template or marks the recommendation as incomplete with reason: missing_template Given a practitioner has excluded an intervention type (e.g., RolloverOffer) When rules suggest that intervention Then the engine skips it and selects the next eligible intervention per playbook priority
Cooldown and Frequency Capping
Given the client received intervention FriendlyNudge via Email 3 days ago and the configured cooldown is 7 days When evaluating recommendations Then the engine does not propose or send FriendlyNudge via Email and records cooldown_active: true Given a frequency cap of 2 interventions per client per 14 days When the cap is reached Then further interventions are suppressed with suppression_reason: frequency_cap and next_eligible_date is calculated Given an alternative intervention exists that does not violate cooldown or caps When suppression occurs for the first-choice intervention Then the engine proposes the next eligible intervention according to playbook priority
Message Copy, CTA, and Slot Generation
Given intervention PrioritySlots and available capacity within the next 10 business days When generating the output Then the message copy resolves variables (client_first_name, service_name, remaining_sessions), includes at least 3 time slots if available, and specifies the timezone correctly Given CTA link generation When producing the CTA Then the URL deep-links to the booking page pre-filtered to the target service and offered windows and includes campaign_id for attribution Given no eligible slots within the priority window When generating the message Then the engine omits the slot list, includes a See more times CTA only, and marks slots_included: false
Auto-run Dispatch
Given account setting Auto-run is enabled and client/channel eligibility is satisfied When a recommendation is generated Then the engine dispatches the message without human approval and records dispatch_timestamp, channel, template_id, recipient_id, and recommendation_id in the audit log Given a rate limit of 100 messages per hour per account When auto-run would exceed the limit Then the engine queues excess messages and sends them in the next available window, recording queue_reason and scheduled_send_time Given a dispatch failure from the channel provider When auto-run attempts to send Then the engine retries per backoff policy up to N attempts and sets final_status to sent or failed with provider_error_code captured
Approve-and-Send Workflow
Given account setting Approve before sending is enabled and a new recommendation is ready When the recommendation is generated Then the engine creates a draft with suggested copy, CTA, slot list, recommended channel, and rule reference, and assigns it to the practitioner for review Given the practitioner edits copy and changes channel within allowed preferences When approving the draft Then the engine revalidates consent, cooldown, and caps and dispatches using the edited content, logging approver_id and changeset Given no action within the configured SLA (e.g., 48 hours before cycle end) When the SLA elapses Then the draft auto-expires with status expired_unapproved or auto-sends if fallback_auto_send is enabled, and the event is recorded in the audit log
Outcome Learning and Weighting Update with Audit Trail
Given interventions are sent and subsequent bookings or replies occur When the nightly learning job runs Then the engine attributes outcomes to intervention rule, channel, and template variant using last-touch within 7 days and updates recommendation weights Given a rule has insufficient data (< N samples) When updating weights Then the engine applies smoothing or retains prior weight and flags low_data without shifting weight by more than 10% Given weights are updated When generating the next day's recommendations Then the engine uses the new weights and the audit log records model_version, rule_id, weight_before, weight_after, sample_size, and timestamp
Smart Slot Promotion
"As a solo practitioner, I want to offer convenient priority times to at-risk clients so that they can quickly self-book and use their remaining sessions."
Description

Creates promotable priority windows by scanning the calendar for idle capacity and near-term availability, reserving a limited number of slots for at-risk clients. Generates single-use booking links with expiration, respects buffers and double-booking safeguards, and releases unclaimed holds automatically. Allows per-client time-window personalization and timezone-aware suggestions. Integrates with SoloPilot self-scheduling, reminders, and calendar sync to maintain accurate availability.

Acceptance Criteria
Calendar Scan Creates Priority Windows
Given the provider has configured working hours, session durations, and buffer rules And external calendars are synced When the system scans availability for the next 14 days (configurable window) Then it identifies candidate slots that meet or exceed session duration plus buffers And it excludes times with any existing events or holds And it limits candidates to within working hours And it records a list of promotable priority windows for downstream reservation
Reserve Limited Slots for At-Risk Client With Personalization
Given a client is flagged at-risk and has preferred time windows configured When generating a promotion for that client Then no more than the configured cap of priority slots are reserved for the client And the reserved slots fall within the client’s preferred windows and target days where possible And if insufficient capacity exists within preferences, the nearest alternative slots within the next 14 days are reserved and marked as alternatives And all reserved slots are visible only via the client’s single-use link and hidden from general self-scheduling
Single-Use Expiring Booking Link
Given a promotion has been created with an expiration timestamp When the system generates a booking link Then the link is unique, single-use, and bound to the intended client And the link expires precisely at the configured timestamp (timezone-aware) And after one successful booking or expiration, the link cannot be used again and shows an appropriate message And upon booking, any remaining holds for that client are automatically released
Timezone-Aware Suggestions and Notifications
Given the provider’s timezone and the client’s timezone are known When promotional times are presented and reminders are sent Then times are displayed to the client in the client’s local timezone with clear timezone labels And holds are created in the provider’s calendar using the provider’s timezone with correct offset mapping And daylight saving transitions are handled so that both parties see the same absolute time And ICS invites and reminders reflect the correct local times for both parties
Double-Booking Safeguards and Buffers Enforced
Given double-booking prevention is enabled and buffer rules are configured And the provider has one or more external calendars synced When priority holds are created or a client books via a promotion link Then no hold or booking is placed that overlaps an existing event or would violate pre/post buffers And if a new external event is synced that would conflict with an existing hold, the system resolves the conflict by canceling or shifting the hold and notifies the provider and affected client And an audit trail entry is recorded for the conflict resolution
Auto-Release of Unclaimed Holds at Expiration
Given reserved holds exist for a promotion When the promotion reaches its expiration time or is canceled by the provider Then all unclaimed holds are released to general availability within 2 minutes And self-scheduling immediately reflects the freed slots And external calendar availability is updated on the next sync cycle And clients attempting to use an expired link see a clear expiry message with a standard scheduling link as fallback (if enabled)
Self-Scheduling and Reminder Integration
Given a client books a reserved slot via the promotion link When the booking is confirmed Then the appointment is created in SoloPilot with the correct service, client, and tags (e.g., Promotion: Smart Slot) And the slot is removed from reserved status and from any other clients’ promotions And standard confirmation and reminder workflows fire per workspace settings And cancel/reschedule actions taken by either party update availability and re-trigger reminders per policy
Nudge Messaging & Templates
"As a solo practitioner, I want ready-to-send, personalized nudges across channels so that clients are reminded to book before their cycle ends."
Description

Provides compliant, multi-channel outreach (email, SMS, in-app) with pre-built, customizable templates and personalization tokens (remaining credits, deadline, booking link). Enforces consent management, quiet hours, rate limits, and per-channel throttling. Tracks deliverability, opens, clicks, replies, and bookings attributed to each message. Supports language variants and tone presets, plus merge fields for practitioner brand. Integrates with Automations to trigger nudges when thresholds are crossed or when a recommended action is approved.

Acceptance Criteria
Consent-Gated Multi-Channel Nudge Delivery
Given a contact with channel-specific consent states (email/SMS/in-app) and a pending Underuse Rescue nudge When the nudge is dispatched Then messages are sent only via channels with active consent and are skipped for channels without consent And an opt-out keyword (e.g., STOP for SMS, unsubscribe link for email) immediately updates the consent state and halts future sends on that channel And an auditable record logs sender, recipient, channel, template, timestamp, consent check result, and outcome per channel
Quiet Hours and Per-Channel Throttling
Given workspace quiet hours set in the recipient’s local time zone and channel rate limits configured per minute/hour/day When a nudge is scheduled during quiet hours Then it is automatically deferred to the earliest allowed send time And sending adheres to per-channel rate limits without exceeding configured thresholds And per-recipient frequency caps (e.g., max 1 nudge per 24h per channel) are enforced And a send reason (immediate, deferred, throttled) is recorded per attempt
Template Personalization Tokens and Brand Merge Fields
Given a selected nudge template with tokens {remaining_credits}, {deadline_date}, {booking_link} and brand fields {practitioner_name}, {brand_logo} When a preview is generated for a target contact Then all tokens resolve using that contact’s and workspace data with correct formatting (dates in locale, URLs tracked) And invalid or unknown tokens prevent save with a descriptive validation error And missing optional values fall back to configured defaults without rendering raw token text And the final rendered content is stored alongside a hash of the template version for attribution
Language Variants and Tone Presets
Given a template with language variants (e.g., en, es) and tone presets (e.g., friendly, professional) When a recipient locale is detected or explicitly set on the send job Then the corresponding language variant is used, falling back to default if unavailable And the selected tone preset adjusts copy according to predefined style rules without altering required tokens And the variant/tone used is recorded for analytics segmentation
Deliverability, Engagement, and Booking Attribution
Given tracking is enabled for the campaign When nudges are delivered Then the system records per-message: delivery status (queued/sent/bounced), opens (email/in-app), clicks (UTM-tagged), replies (SMS/email), and booking conversions using signed, unique links And bookings are attributed to the last clicked or last delivered nudge within a configurable attribution window And aggregated metrics are available per template, channel, campaign, and recipient segment with CSV export And bounce/complaint events automatically suppress the affected channel for that recipient
Automation Trigger and Approval Flow Integration
Given an Underuse Rescue threshold is crossed or a recommended action is approved in Automations When trigger conditions are met Then a nudge is created with the mapped template, audience, channel priorities, and scheduled send time And optional human approval holds the nudge until approved or rejected with audit trail And deduplication prevents multiple nudges for the same recipient and reason within a defined cooldown window And trigger source, rule version, and cooldown decision are recorded
Provider Error Handling and Retry Backoff
Given a transient provider error (e.g., 429, 5xx) occurs during send When the send attempt fails Then the system retries with exponential backoff up to the configured max attempts per channel without breaching rate limits And permanent errors (e.g., hard bounce, invalid number) do not retry and update recipient/channel status accordingly And all retries and outcomes are logged and visible in message details
Rollover Offer Management
"As a solo practitioner, I want to grant limited rollover credits when appropriate so that clients still realize their value without setting a precedent for unlimited carryover."
Description

Enables creation of limited rollover credits when justified by risk and policy, with rules for caps, expiration dates, eligible services, and one-time or per-cycle allowances. Requires explicit approval or auto-approval based on playbook settings. Automatically reflects rollover credits in the client ledger, booking eligibility, and invoicing/balance displays. Includes client notifications, acceptance tracking, and an auditable history to prevent abuse and ensure policy compliance.

Acceptance Criteria
Draft and Submit Rollover Offer with Policy Rules
Given active policy settings defining cap limits, expiration window, eligible services, and allowance scope (one-time or per-cycle) When a user drafts and submits a rollover offer for an underutilized client Then the system enforces required fields (cap amount/units, expiration date/time, eligible services, allowance scope, justification code) And validates entries against policy thresholds (max cap, min/max expiration window, allowed services) And blocks submission with field-level error messages when a rule is violated And on success, the offer is saved with the configured cap, expiration, eligible services, and allowance scope
Auto-Approval and Manual Approval Flow
Given playbook rules that auto-approve offers meeting specified risk band and threshold criteria When a submitted offer satisfies the auto-approval rules Then the system sets status to Approved, records approver as System, stores the rule ID used, and notifies the owner And when a submitted offer exceeds thresholds or falls outside auto-approval conditions Then the system sets status to Pending Approval, notifies the approver group, and prevents client notification until approval is recorded
Ledger, Booking Eligibility, and Invoicing Reflection
Given an Approved rollover offer When the offer becomes active Then the client ledger records a Credit entry of type Rollover with amount/units, expiration date, and reference to the offer ID And scheduling shows available rollover as an applicable balance for eligible services only And invoicing applies the rollover to eligible charges up to the remaining cap and displays a Rollover Credit line item; remaining unused rollover is preserved and shown separately from cash balance
Client Notification and Acceptance Tracking
Given an Approved rollover offer that requires client acceptance per playbook When the offer is sent to the client Then the system queues notifications via configured channels (email/in-app) with terms, cap, expiration, eligible services, and a CTA, and records delivery status And when the client accepts before expiration Then the system time-stamps acceptance, captures identity (email/account), and activates the offer And when the client declines or does not respond by expiration Then the offer is marked Declined or Expired respectively and is ineligible for application
Booking Application and Enforcement
Given an Active rollover with remaining balance and an eligible services list When a booking is created for an eligible service before the offer expiration Then the system applies rollover up to the available cap and shows the application on the booking confirmation and draft invoice And when the booking is for an ineligible service, the offer is expired, or the cap would be exceeded Then the rollover is not applied and the UI displays a specific reason (Ineligible Service, Expired Offer, Cap Exceeded)
Audit Trail and History Visibility
Given any lifecycle event on a rollover offer (create, edit, approve/deny, notify, accept/decline, apply, expire, revoke) When the event occurs Then an immutable audit record is written with timestamp, actor, action, previous/new values, related policy/playbook ID, and client ID And authorized users can view a chronological history on the client and offer detail screens and export it as CSV
Policy Compliance and Abuse Prevention
Given global policy limits (max rollovers per client per cycle, maximum cumulative rollover value, blackout services) When a rollover offer is created or modified Then the system evaluates cumulative rollovers within the cycle and blocks actions that breach limits with explicit error messages And prevents stacking overlapping offers for the same service/cycle when policy disallows stacking And when overrides are permitted, requires approver justification and logs the override reason and rule reference
Rescue Outcome Analytics & Experimentation
"As a solo practitioner, I want to see which rescue tactics work best so that I can continuously improve retention and utilization."
Description

Delivers dashboards and reports showing utilization uplift, retained revenue, prevented churn, time-to-booking after outreach, and per-intervention success rates. Supports A/B testing of templates, channels, and timing, with cohort and service-level breakdowns. Provides exportable CSV and event streams for BI tools. Surfaces insights and recommendations (e.g., best send times, most effective offers) and closes the loop by informing the recommendation engine.

Acceptance Criteria
Executive Dashboard Shows Rescue Outcome KPIs
- Given a workspace with rescue interventions and outcomes in the selected date range, When the user opens the Rescue Outcomes dashboard, Then the KPIs displayed include: utilization uplift %, retained revenue $, prevented churn count and $, median time-to-booking after outreach, and per-intervention success rate %. - Given new outcomes are recorded (e.g., booking created, invoice paid), When 15 minutes have elapsed, Then the dashboard reflects the new data without manual refresh. - Given the user applies date range, cohort, and service filters, When Apply is clicked, Then all KPI tiles and charts recalculate to match the filters and complete in under 2 seconds for ≤100k rows (under 5 seconds for >100k rows). - Given a metric’s sample size is <30, When the tile renders, Then the tile shows "Insufficient data" with current sample size and does not display a percentage. - Given a user hovers any KPI, When the tooltip appears, Then it shows the formula, numerator/denominator counts, and active filters used.
A/B Test Setup and Randomization for Rescue Interventions
- Given an experiment is created with variants across templates, channels, or send times and a success definition (booking within X days), When the experiment is launched, Then users in the target segment are randomly assigned to variants according to configured allocation (including optional control/holdout) with no user receiving multiple variants for the same outreach. - Given an active experiment, When assignments occur, Then assignment events are logged with experiment_id, variant_id, user_id/client_id, timestamp, and allocation weight for auditability. - Given variant outcomes accrue, When each variant reaches ≥100 unique recipients and the minimum duration of 7 days, Then the system computes statistical significance at 95% confidence (two-tailed) and displays effect size and confidence interval. - Given an experiment is paused or ended, When new outreach is triggered, Then no new assignments are created and all traffic is routed per the configured fallback. - Given a winner is declared, When viewing the experiment report, Then the winning variant is labeled with uplift %, p-value, and sample size, and the date/time it met significance.
Cohort and Service-Level Breakdown Filters
- Given the user selects cohort filters (e.g., signup month, lifecycle stage, plan tier) and service types, When filters are applied, Then all breakdown charts and tables show per-cohort and per-service metrics for utilization uplift, success rate, and retained revenue. - Given multiple cohorts/services are selected, When totals are displayed, Then the aggregate total equals the sum of visible segments within ±0.1% rounding tolerance. - Given a user saves a report view, When the saved view is reopened, Then the exact filters and segment definitions persist. - Given filters are active, When the user exports data (CSV or event stream), Then only data matching the filters is included.
CSV Export of Outcome and Experiment Data
- Given the user clicks Export CSV on any Rescue Outcomes report, When the export completes, Then the CSV contains a header row and at minimum the columns: workspace_id, outreach_id, client_id, cohort_id, service_id, intervention_type, channel, template_id, experiment_id, variant_id, sent_at, opened_at, clicked_at, booked_at, time_to_booking_minutes, outcome (booked_within_window), prevented_churn, retained_revenue_amount, currency. - Given filters and date range are set, When the CSV is generated, Then only rows matching the current filters and range are included. - Given an export exceeds 250k rows, When the user confirms export, Then an asynchronous job runs and a download link is delivered within 15 minutes; the link expires after 7 days. - Given PII protections are enabled by default, When the CSV is generated, Then no names, emails, or phone numbers are included unless the user explicitly checks “Include PII”, and a warning modal must be acknowledged. - Given CSV standards, When the file is opened, Then it is UTF-8 encoded, RFC 4180 compliant, uses comma as delimiter, and newline as \n.
Real-Time Event Stream for BI Integration
- Given a workspace configures a webhook destination with a shared secret, When rescue-related events occur (outreach_sent, outreach_opened, outreach_clicked, booking_created, invoice_paid, experiment_assigned, experiment_winner_declared), Then events are POSTed within 60 seconds of occurrence. - Given an event is delivered, When the receiver validates headers, Then an HMAC-SHA256 signature (X-SoloPilot-Signature) over the body with the shared secret is present and correct. - Given transient delivery failures, When the destination responds with non-2xx, Then retries occur up to 5 times with exponential backoff over 24 hours; events include an idempotency_key to ensure at-least-once delivery without duplication. - Given an event payload, When parsed, Then it contains event_id, event_type, occurred_at (ISO 8601 UTC), workspace_id, client_id, intervention metadata (type, channel, template_id), experiment metadata (experiment_id, variant_id, allocation), monetary fields with currency, and current filterable dimensions (cohort_id, service_id). - Given a destination is disabled or in test mode, When events occur, Then no production deliveries happen and a health status is shown in the UI.
Insight Generation: Best Send Times and Offer Effectiveness
- Given ≥500 outreaches in the last 90 days, When nightly insight computation runs at 02:00 local workspace time, Then the system surfaces the top 3 send time windows (day-of-week x hour-of-day) per channel with estimated uplift and 95% confidence intervals. - Given offer variants exist, When insights are refreshed, Then the top 3 offers by conversion rate are shown with sample sizes and effect sizes. - Given an insight card is opened, When the "Why" panel is expanded, Then the data window, sample size, methodology summary, and last updated timestamp are displayed. - Given a user clicks "Apply as default", When confirmation is accepted, Then the selected send time(s) and/or offer(s) are saved as the default Underuse Rescue strategy for the chosen segment and versioned with a change log entry.
Closed-Loop Feedback to Recommendation Engine
- Given an experiment reaches statistical significance, When the next recommendation cycle runs (hourly), Then the recommendation engine updates weights or rules to favor the winning variant for matching segments. - Given a recommendation update is applied, When the audit log is viewed, Then an entry exists with version_id, timestamp, experiment_id, affected segment definition, old vs new weights, and expected uplift. - Given guardrails are enabled, When the live conversion rate degrades by ≥20% versus the prior 7-day baseline within 48 hours of a change, Then the system automatically rolls back to the previous version and alerts the workspace owner. - Given model updates occur, When exports are requested, Then a CSV of recommendation version history for the selected period can be downloaded with fields sufficient to reproduce changes.

PO Cap Guard

Lets you set PO or budget caps per retainer, alerts you as you approach limits, and automatically pauses over-cap auto-billing. Sends clients a pre-filled extension request or top-up option, ensuring compliance for enterprise accounts while keeping delivery predictable.

Requirements

Retainer Cap Configuration & Enforcement
"As an account owner managing enterprise retainers, I want to define enforceable PO/budget caps per retainer so that billing cannot exceed authorized limits."
Description

Enable admins to define enforceable budget/PO caps per retainer with support for currency amount, hours, or hybrid caps; effective dates; PO number and vendor fields; multi-currency with explicit tax-included/excluded handling; and cap rounding rules. Caps are configured within Retainer settings and applied consistently across SoloPilot’s session-to-invoice flow, manual invoices, expenses, and scheduled billing. The system blocks automatic charges that would exceed the cap, displays remaining capacity inline (progress bar and numeric), and flags attempts to overrun with clear guidance and links to request an extension. Config includes default threshold alerts, cap behavior at limit (block/queue/allow with override), and visibility controls for internal vs client portals.

Acceptance Criteria
Cap Type Configuration and Rounding Rules
Given an admin edits a Retainer’s Cap Settings, When Cap Type is set to Currency, Then Currency, Cap Amount, and a Currency Rounding Rule are required and Save succeeds. Given Cap Type is set to Hours, When saving, Then Hours Amount and a Time Rounding Rule are required and Save succeeds. Given Cap Type is set to Hybrid, When saving, Then both Currency+Amount (with Currency Rounding Rule) and Hours+Amount (with Time Rounding Rule) are required and Save succeeds. Given rounding rules are configured, When utilization is calculated anywhere in the app, Then the selected rounding rules are applied consistently to all displayed values and billing computations. Given required fields are missing or invalid, When Save is attempted, Then Save is blocked and inline validation errors identify the missing/invalid fields.
Effective Dates and Non-Overlapping Cap Periods
Given an existing cap period, When creating a new cap for the same retainer, Then the new Start/End dates must not overlap any active cap period and overlapping saves are rejected. Given a cap with a future Start Date, When billing occurs before the Start Date, Then those transactions do not consume the cap. Given a cap with an End Date, When billing occurs after the End Date, Then those transactions do not consume the cap. Given the retainer time zone, When saving Start/End dates, Then validation and storage use the retainer’s time zone and UI reflects that time zone.
Multi-Currency, Tax Handling, and PO/Vendor Metadata
Given cap currency differs from an invoice currency, When utilization is computed from that invoice, Then the amount is converted using the organization’s configured FX rate as of the invoice posting date and rounded per the currency rounding rule. Given Tax Handling is set to Tax Included, When computing cap consumption, Then taxes are included in utilization and the UI labels the capacity as tax-included. Given Tax Handling is set to Tax Excluded, When computing cap consumption, Then taxes are excluded from utilization and the UI labels the capacity as tax-excluded. Given PO Number and Vendor fields are populated in the cap settings, When invoices are generated under the retainer, Then the PO Number and Vendor appear on the invoice and are stored with the cap configuration for auditing.
Enforcement Across Session-to-Invoice, Manual Invoices, Expenses, and Scheduled Billing
Given Cap Behavior is Block, When an automatic or manual charge would exceed remaining cap, Then the system prevents the charge, displays an over-cap message with guidance, and logs a cap-block event. Given Cap Behavior is Queue, When a charge would exceed remaining cap, Then the system queues the charge for later processing and notifies the retainer owner. Given Cap Behavior is Allow with Override, When a charge would exceed remaining cap, Then an override prompt is shown and only users with Override permission can proceed; the override decision is audit logged. Given any billing flow (session-to-invoice, manual invoice, expense, scheduled billing), When the transaction is prepared, Then the cap check executes before finalization or auto-charge and applies the configured behavior.
Remaining Capacity Display (Progress Bar and Numeric)
Given a retainer with an active cap, When viewing the retainer dashboard or drafting an invoice, Then a progress bar and numeric remaining capacity are shown and reflect current utilization in real time as line items change. Given a hybrid cap, When viewing capacity, Then separate indicators for currency and hours are displayed with their respective remaining amounts. Given alert thresholds are configured, When remaining capacity crosses a threshold, Then the progress bar color changes accordingly and a tooltip shows remaining capacity and whether tax is included or excluded. Given client portal visibility for capacity is disabled, When a client views the portal, Then capacity indicators are hidden; when enabled, they are shown.
Threshold Alerts and Notifications
Given default or custom thresholds (e.g., 50%, 75%, 90%), When utilization crosses a threshold within a cap period, Then exactly one in-app notification is generated per threshold per period and an email notification is sent to the retainer owner. Given client visibility is disabled for alerts, When thresholds are crossed, Then no client-facing notifications are sent; when enabled, client notifications are sent according to settings. Given an alert is generated, When viewing the retainer activity log, Then the event records timestamp, threshold crossed, remaining capacity, and recipient(s).
Overrun Attempt Handling and Extension/Top-Up Request
Given an action would exceed the cap, When the user attempts to proceed, Then a modal shows the overage amount and provides options to Split to Fit, Queue Remainder, or Request Extension/Top-Up. Given Request Extension/Top-Up is selected, When submitting, Then the request is pre-filled with retainer, PO number, vendor, current cap, remaining capacity, and proposed top-up and is sent to the configured client approver or approval queue with a tracking link. Given Split to Fit is selected, When confirmed, Then the invoice or charge is adjusted to consume only the remaining capacity and the remainder is left in draft or queued per cap behavior. Given any overrun handling action completes, When viewing the retainer activity log, Then the outcome is recorded with user, timestamp, action taken, and any request link.
Real-time Utilization Tracking & Rollup
"As a consultant delivering against a retainer, I want accurate real-time cap utilization so that I can plan sessions and scope without risking overages."
Description

Continuously calculate cap utilization by aggregating time entries, session charges, fixed-fee items, expenses, and posted invoices, distinguishing pending vs posted amounts. Support backdated edits, refunds/credits, write-offs, and tax treatment aligned to retainer settings. Provide a single source of truth for Remaining/Used/Committed values, with recalculation on edits and idempotent updates. Surface utilization in context across the app (retainer overview, scheduling, session notes, invoice creation) with a progress indicator and projected runout date based on recent burn rate.

Acceptance Criteria
Real-time rollup updates on create, update, and delete
- Given a retainer with a cap amount configured and zero utilization When a user saves a new billable time entry of $100 linked to the retainer Then Committed increases by $100 and Used (Posted) remains unchanged within 2 seconds and Remaining = Cap - (Used (Posted) + Committed) - Given the above time entry When the user posts an invoice that includes the time entry Then Committed decreases by $100 and Used (Posted) increases by $100 within 2 seconds and Remaining reflects the new totals - Given a billable item in Committed When the amount, quantity, or tax is edited Then Committed and Remaining recalculate within 2 seconds using the retainer tax setting (include tax in cap if enabled; exclude if disabled) - Given a billable item in Committed When it is deleted or marked non-billable or exclude-from-cap Then Committed decreases accordingly within 2 seconds and rollup equals the sum of remaining items within $0.01 - Given multiple billable item types (time entries, session charges, fixed-fee items, expenses) When they are created, updated, or deleted Then the rollup aggregates all types exactly once and amounts are rounded to 2 decimals (half-up)
Pending vs posted segregation with no double-counting
- Given unbilled/approved billable items and posted invoices linked to the same retainer When viewing utilization Then Used (Posted) equals the sum of posted invoice line items net of refunds/credits/write-offs, Committed equals the sum of unbilled/approved items not yet invoiced, and no item appears in both - Given a billable item currently counted in Committed When it is added to a posted invoice Then it is removed from Committed and included in Used (Posted) exactly once - Given a billable item flagged exclude-from-cap or non-billable When viewing utilization Then it is excluded from both Used (Posted) and Committed - Given amounts are displayed by type When viewing the breakdown Then the sums by type (time, session charges, fixed-fee, expenses, invoice adjustments) equal the displayed totals within $0.01 - Given a single retainer currency When viewing utilization Then all amounts are shown in that currency and equal the sum of underlying lines within $0.01
Backdated edits and idempotent recalculation
- Given a backdated billable item (effective date in the past) is created today and linked to the retainer When it is saved Then utilization totals update within 2 seconds and the audit history records the effective date and change source - Given an import or integration replays the same item with the same idempotency key or external_id When processed repeatedly Then utilization totals are unchanged after the first successful apply - Given a posted invoice has its date backdated without changing amounts When saved Then utilization totals remain numerically identical and reporting reflects the new date - Given an item is voided and re-issued with a new identifier but the same external_id When processed Then the net effect on Used (Posted) equals a single replace and no double-count occurs
Adjustments: refunds, credits, write-offs, and tax handling
- Given a posted invoice of $100 subtotal with $10 tax linked to a retainer with tax-counts-toward-cap enabled When viewing utilization Then Used (Posted) increases by $110 - Given the same invoice but the retainer has tax-counts-toward-cap disabled When viewing utilization Then Used (Posted) increases by $100 and tax is excluded from cap consumption - Given a $30 refund or credit memo is applied to the posted invoice When processed Then Used (Posted) decreases by $30 within 2 seconds and Remaining increases accordingly without exceeding the cap - Given a $20 write-off is recorded against the posted invoice When processed Then Used (Posted) decreases by $20 within 2 seconds and Used (Posted) never drops below $0 - Given multiple adjustments (refunds, credits, write-offs) are applied in any order When viewing utilization Then the net Used (Posted) equals original posted amount minus the sum of adjustments, respecting the retainer tax setting and rounding to 2 decimals
Contextual surfacing with progress indicator and runout projection
- Given an active retainer When viewing the Retainer Overview Then a progress indicator displays Posted Used and Committed segments, Remaining, and a projected runout date - Given utilization is displayed in Scheduling, Session Notes, and Invoice Creation When viewing each context Then the same Posted Used, Committed, Remaining, and runout projection values are shown as on the Retainer Overview within the same refresh cycle - Given a trailing 14 calendar day window with at least 3 days of burn > $0 exists When calculating projection Then projected runout date = today + (Remaining / average daily burn of Posted Used + Committed over the window), rounded up to the next calendar day - Given the average daily burn is $0 or fewer than 3 non-zero-burn days in the window When calculating projection Then the projected runout date displays as Not enough data - Given the estimated charge of a session or invoice draft in context When its amount would cause Posted Used + Committed to exceed the cap Then the progress indicator shows an over-cap state and the projected overage amount is displayed
Concurrency, atomicity, and performance under load
- Given two users concurrently create or modify billable items linked to the same retainer When both saves succeed Then the final totals equal the result of applying both changes exactly once with no double-count and both users see updated totals within 2 seconds (P95) and 5 seconds (P99) - Given rapid sequential edits to the same item When the last write wins by updated_at timestamp Then utilization reflects only the latest persisted values - Given a batch import updates 500 items for one retainer within 60 seconds When processing completes Then totals are correct and intermediate reads may be stale but converge to correct values within 60 seconds of the last processed change - Given a user saves a change When immediately querying utilization from the same session Then the user observes read-your-writes consistency - Given any recalculation When it runs Then the operation completes within 200 ms (P50), 2 seconds (P95), and 5 seconds (P99) for retainers with up to 10,000 linked items
Threshold Alerts & Notifications
"As a project owner, I want proactive alerts as we approach the cap so that I can either slow delivery or initiate a cap extension before we breach."
Description

Offer configurable alert thresholds (e.g., 50%, 75%, 90%, 100%) with recipient rules for internal roles and optional client contacts. Deliver notifications via in-app banners, email, Slack, and webhook callbacks with throttling, digesting, and quiet hours. Alerts include retainer context, PO number, remaining capacity, projected runout, and a deep link to request an extension or top-up. Respect time zones and localization settings, and ensure idempotent delivery with retry on failure.

Acceptance Criteria
Configurable Threshold Triggers per Retainer
Given a retainer with a PO/budget cap and alert thresholds configured (e.g., 50%, 75%, 90%, 100% and a custom 60%) When cumulative usage first crosses any configured threshold Then exactly one alert event is created for that threshold and retainer And the alert records the threshold crossed, current usage, remaining capacity, and projected runout And changing the threshold configuration affects only alerts generated after the change
Multi-Channel Notification Delivery (In-App, Email, Slack, Webhook)
Given a threshold alert event exists and channel preferences are enabled for in-app banners, email, Slack, and webhook When the alert is processed for delivery Then one notification per enabled channel is sent to resolved recipients/destinations And a failure in any one channel does not block delivery attempts for other channels And each channel's delivery outcome is logged with timestamp and status
Recipient Rules for Internal Roles and Optional Client Contacts
Given recipient rules specify internal roles (e.g., Account Owner, Billing, Project Manager) and optional client contacts flagged to receive budget alerts When a threshold alert event is generated for a retainer Then recipients are resolved according to the rules, including only users with access to the retainer and client contacts explicitly opted in And per-recipient per-channel opt-outs are respected And if no recipients are resolved for a channel, that channel is skipped without error
Quiet Hours, Throttling, and Digesting Behavior
Given quiet hours are configured per recipient and throttling/digest settings are active When multiple alerts are generated during a recipient's quiet hours Then email and Slack notifications are deferred and included in a single digest sent at the next allowed send window And webhook callbacks are not deferred by quiet hours And in-app banners are displayed on next app view, not pushed during quiet hours And multiple alerts of the same threshold for the same retainer within the throttle window result in a single notification per recipient per channel
Alert Payload Completeness and Deep Link to Extension/Top-Up
Given a threshold alert is delivered via any channel When the recipient views the notification content Then the payload includes retainer name, PO number, threshold hit, current usage, remaining capacity, projected runout, and a deep link to request an extension or top-up And following the deep link opens the extension/top-up request with the retainer pre-selected and relevant fields pre-filled
Time Zone and Localization Compliance
Given recipients have time zone and locale preferences When alerts are generated and delivered Then quiet hours, send times, and projected runout dates respect each recipient's time zone (including DST transitions) And dates, times, numbers, and currencies in the notification are formatted per the recipient's locale And the same alert displays consistent values across channels for the same recipient
Idempotent Delivery with Retry on Failure
Given each alert event has a deterministic idempotency key for the retainer-threshold occurrence When a channel delivery is retried due to network errors or transient 5xx responses Then no recipient receives duplicate notifications for the same alert per channel And 4xx responses do not trigger retries And webhook deliveries include the idempotency key so consumers can deduplicate And all retry attempts and final outcomes are recorded
Auto-Pause/Resume Over-Cap Billing
"As a finance admin, I want over-cap billing to pause automatically and resume once the cap is extended so that compliance is maintained without manual policing."
Description

When a cap is reached, automatically pause auto-billing pathways (session-to-invoice, scheduled invoices, recurring charges) and queue would-be charges with clear status. Display a prominent ‘Over Cap’ state on the retainer and prevent accidental charge attempts. Allow role-based manual override with reason capture and optional client acknowledgement. Automatically resume billing when a top-up or new PO is applied and reprocess queued items in order with guardrails to avoid double-charging.

Acceptance Criteria
Auto-Pause at Cap Across All Billing Pathways
Given a retainer with a cap amount X and total billed-to-date >= X When the system attempts to create a charge from session-to-invoice, scheduled invoice, or recurring charge Then the attempt is not sent to the payment processor and is placed in a queue with status "Queued—Over Cap", the original source, amount, currency, and a FIFO sequence number And the retainer state flips to "Over Cap (Paused)" within 5 seconds of the first blocked attempt And no chargeId is created and no client notification of a charge is emitted And the action is audit logged with actor (system), timestamp, source, amount, and remaining cap (<= 0) And if multiple attempts occur concurrently, at most the attempt that keeps total billed <= X is processed; all others are queued
Prominent Over Cap State and Charge Prevention
Given a retainer in state "Over Cap (Paused)" When any user opens the retainer detail or list view Then a prominent "Over Cap" badge and banner are visible, and the next charge ETA/queue count is displayed And charge/collect actions (including "Charge Now" and "Generate Invoice & Charge") are disabled in the UI with an explanatory tooltip And API attempts to create charges for this retainer return HTTP 409 with error code OVER_CAP and are not sent to the processor nor queued (manual attempts are blocked, not queued) And keyboard and screen-reader users receive the same information via aria-live and labels
Role-Based Manual Override with Reason and Optional Client Acknowledgement
Given a retainer in state "Over Cap (Paused)" When a user with role in {Owner, Billing Admin} selects "Override and Charge" Then the system requires a reason (min 10 characters) and captures it with timestamp and userId And the user can toggle "Require client acknowledgement"; if enabled, a pre-filled approval request is sent to the client billing contact, and processing waits for explicit approval And until approved, the item shows status "Pending Client Approval" with expiry at 14 days; on expiry, it reverts to queued And on approval, the charge is processed with idempotency tied to the original itemId; on success, the audit log records reason, approver identity, and client acknowledgement id And users without required roles do not see the override option and receive 403 on API
Automatic Resume and Ordered Reprocessing After Top-Up or New PO
Given a retainer in state "Over Cap (Paused)" with N queued items and a posted top-up or new PO that increases available budget > 0 When the top-up/PO is applied Then the retainer state transitions to "Active" and the system reprocesses queued items strictly by createdAt ascending And each item is attempted if its amount <= current available budget; on success, available budget decreases accordingly and the item status becomes "Charged" And if an item fails at the processor, it is marked "Error—Payment" and processing continues for the next item while budget allows And before sending any item, the system checks for an existing charge with the item's idempotency key; if found, the item is marked "Skipped—Already Charged" and not sent, preventing double-charge And transient failures are retried up to 3 times with exponential backoff; on exhaustion, status is "Error—Retry Exhausted"
Partial Top-Up Handling and Residual Queue
Given a retainer with queued items and a top-up that is insufficient to clear all queued items When reprocessing runs Then items are processed in FIFO order until remaining budget is less than the next item's amount And processed items move to "Charged"; unprocessed items remain in the queue with their original order and status "Queued—Insufficient Budget" And the retainer remains "Active" if remaining budget > 0; subsequent auto-billing attempts are charged only if amount <= remaining budget, otherwise they are queued And UI surfaces remaining budget and queue count after reprocessing completes
Concurrency and Idempotency Guardrails at Cap Boundary
Given multiple billing pathways generate charge attempts within 1 second when remaining budget is <= the smallest attempt amount When the system evaluates the attempts Then an atomic check-and-queue operation ensures the sum of charged amounts never exceeds the cap And at most one attempt that fits remaining budget (if any) is processed, and all others are queued And repeat submissions of the same source item (same sessionId/invoiceId/recurringId) are deduplicated via idempotency key, resulting in a single charge and additional attempts marked "Skipped—Duplicate" And all outcomes are audit logged with correlationId for the burst
Client Cap Extension/Top-up Workflow
"As an account manager, I want to send clients a one-click cap extension request so that work can continue without billing gaps or compliance risks."
Description

Generate a pre-filled client request containing current spend, remaining balance, and recommended top-up options (fixed amount, additional hours, new PO). Send via branded email with a secure approval link. Support client-side actions: approve extension, choose top-up amount, upload new PO, or pay top-up via card/ACH. On approval, update the retainer cap automatically, attach documents, notify stakeholders, and optionally resume paused billing. Include expiration handling, multi-approver sequences, and an auditable confirmation record.

Acceptance Criteria
Pre-filled Extension/Top-up Request Generation
Given a retainer has an active cap and tracked spend When a user initiates an Extension/Top-up request or a configured threshold is reached Then the request is generated showing total cap, current spend-to-date, remaining balance, and percent remaining with a timestamped snapshot And recommended top-up options are displayed: fixed amounts (e.g., 10%, 25%, custom), additional hours (computed using the retainer’s default hourly rate), and a New PO option And all monetary values use the retainer currency with two decimals and thousands separators And negative remaining balances are labeled "Over by" and displayed distinctly And the request is saved as Draft with a unique request ID and version number until sent
Branded Email Delivery with Secure Approval Link
Given a draft request is reviewed and the user selects Send When the system dispatches the client email Then the email uses workspace branding (logo, colors, sender/reply-to) and includes the client name and retainer name in the subject And the email body summarizes cap, spend, remaining, and recommended options And a single-use, HTTPS approval link with a token tied to the request ID and expiration is included And the approval link expires precisely at the request’s expiration date/time and shows an "Expired" message thereafter And the system records send timestamp, recipients, and message ID for traceability
Client Approval, Top-up Selection, and Payment
Given a recipient opens the approval link before expiration When the approval page loads Then the page displays current cap, spend, remaining balance, and recommended top-up options And the client can choose: Approve extension, select a top-up amount (predefined or custom within configured min/max), upload a new PO, pay top-up via card or ACH, or Decline And entering a custom amount validates currency format and must be greater than or equal to 0 and not exceed configured maximums And card/ACH payments are processed through the connected payment gateway; on success a receipt is generated and on failure a clear error is shown with no cap update And a New PO requires PO number, amount, and a file upload (PDF/JPG/PNG up to the configured size limit); invalid files are rejected And the approver must provide full name and corporate email; optional note is captured
Automatic Cap Update, Document Attachment, and Stakeholder Notification
Given an approval is submitted and any required payment has succeeded When the system processes the approval Then the retainer cap is updated immediately by the approved top-up amount or replaced by the new PO amount, per the selected option And all uploaded documents (PO, approval artifact) are stored, checksumed, and attached to both the retainer and the request record And the request status transitions to Approved with the effective cap value recorded And stakeholders (account owner, billing contact, project manager) receive notifications summarizing the change, approver identity, and any attachments And existing invoices remain unchanged; future billing respects the updated cap
Optional Billing Resume Post-Approval
Given auto-billing was paused due to cap reached and the request was sent with "Resume billing after approval" enabled When the approval is finalized Then auto-billing resumes and the next scheduled billing run includes eligible charges under the updated cap And if the option was not enabled, auto-billing remains paused and the owner is prompted to resume manually And the resume/no-resume outcome is logged with timestamp and actor
Request Expiration and Auto-Expiry Handling
Given a request has a set expiration date/time When the current time reaches T-72h and T-24h before expiration Then reminder notifications are sent to the client recipient(s) and internal owner with the request summary and link When the current time passes the expiration Then the approval link becomes invalid and displays an Expired message with owner contact details And the request status updates to Expired and no cap changes are applied And the owner receives an alert with a one-click option to duplicate and resend the request And all reminder and expiry events are logged
Multi-Approver Sequence and Audit Trail
Given a request requires multiple approvers in a defined sequence When Approver 1 approves Then Approver 2 is invited, and so on until the final approver And any approver may Reject with a required reason; rejection cancels the request and notifies stakeholders And approver windows are bounded by the overall expiration; overdue approvers trigger escalation notifications per configuration When the final approver approves and any payment is confirmed Then the cap update is applied and a tamper-evident confirmation record is generated including request ID, approver names/emails, actions (approve/reject), timestamps, IP addresses, amounts, and document hashes And the confirmation record is accessible in the retainer’s audit log and exportable as PDF/JSON
Compliance Audit Trail & PO Document Management
"As a compliance officer, I need a complete audit trail and PO documentation so that enterprise procurement and audit requirements are satisfied."
Description

Maintain an immutable audit log of all cap-related events: creations, edits, overrides, pause/resume actions, alerts sent, approvals, and payments, with timestamps, user IDs, IPs, and diffs. Store and version PO documents and client approvals, with role-based access controls and retention policies. Provide export to CSV/PDF and webhook events for enterprise systems. Ensure data is tamper-evident and aligned with SOC 2 style controls and enterprise procurement expectations.

Acceptance Criteria
Immutable Audit Log for Cap Lifecycle Events
Given PO Cap Guard is enabled for a retainer When any of the following occurs: cap creation, cap edit, override applied, auto-billing pause, auto-billing resume, alert sent, client approval recorded, or payment applied Then an audit log entry is appended with fields: event_type, retainer_id, cap_id/po_number, timestamp_utc (ISO 8601), actor_user_id or "system", actor_ip, request_id, and old_values/new_values diffs where applicable And then the entry_id is unique and monotonically increasing per retainer And then attempts to update or delete an existing audit entry are blocked and a security_event is logged And then 100% of a seeded test suite of events is captured with no omissions
Tamper-Evident Hash Chain Verification
Given the audit log contains N entries for a retainer When a verification job computes each record's hash and prev_hash chain from genesis Then the chain verification returns verified=true When any single field in a stored entry is altered Then verification returns verified=false and identifies the first broken link And then exports and reports include a verification_status of verified or failed And then a failed verification triggers a high-severity alert to owners within 5 minutes And then a daily checkpoint hash of the prior day's chain is stored immutably and logged at 00:00 UTC
PO Document Upload, Versioning, and Integrity
Given a user with document-upload permission When they upload a PO or client approval file up to 25 MB of type PDF, DOCX, PNG, or JPEG Then the system stores it as version v+1 with metadata: version_number, uploader_user_id, uploaded_at_utc, mime_type, filename, size_bytes, sha256_checksum, and po_number And then downloading the file reproduces the stored sha256_checksum And then previous versions remain accessible read-only and cannot be altered or deleted except by retention purge And then replacing a document creates a new version and updates the current pointer without modifying prior versions And then an approval record captures approver_identity, approved_at_utc, and source (e-sign/link/upload)
Role-Based Access Control for Audit and Documents
Given organization roles Owner, Billing Admin, Contributor, Read Only, and Client Contact When accessing audit logs Then only Owner and Billing Admin can view full audit logs; Contributor can view entries they authored; Read Only can view but not export; Client Contact has no audit access When accessing PO documents Then Owner and Billing Admin can upload, version, export, and share; Contributor can upload new versions; Read Only can view/download; Client Contact can only access documents shared via an expiring link And then all access attempts (allowed or denied) are logged with user_id, action, resource_id, outcome, and timestamp_utc And then permission changes are audited with before/after diffs
Retention Policy and Legal Hold Enforcement
Given an organization-level retention policy of 7 years for audit logs and documents When records exceed the retention period and are not on legal hold Then a scheduled purge permanently deletes eligible records and logs a purge_summary with counts and IDs And then records on legal hold are excluded from purge until the hold is removed And then users without Owner or Billing Admin cannot modify retention settings or legal holds And then attempts to delete documents or audit entries outside the purge process are blocked and logged And then exports indicate when portions of history have been purged with redacted=true and date ranges
Export Audit Trail and Documents
Given an authorized user selects a date range, retainer, and PO number When exporting the audit trail to CSV Then the file contains all matching entries with columns for all required fields including diffs, ordered by timestamp_utc, and the row count equals the query count When exporting to PDF Then the document includes a human-readable timeline with diffs, page numbers, and a verification_status, and is digitally signed or accompanied by a manifest containing a sha256 checksum When the export exceeds 50,000 rows Then the system streams the CSV in chunks and delivers a zipped archive And then document versions can be exported alongside the audit as a ZIP bundle with metadata.json And then all exports are logged with parameters, requester, and checksum
Webhook Event Delivery and Idempotency
Given a webhook endpoint with a shared secret is configured When any cap-related event is logged Then a POST is sent within 30 seconds with JSON payload containing event_type, ids, timestamp_utc, diffs, document metadata if applicable, and an idempotency_key And then the request includes an X-SoloPilot-Signature header (HMAC-SHA256 of the body) that validates with the shared secret And then on non-2xx responses the system retries with exponential backoff for up to 24 hours, preserving idempotency_key; a 2xx response stops retries; a 410 permanently stops And then duplicate deliveries with the same idempotency_key are recorded as duplicates and do not create duplicate downstream records And then delivery logs are visible to Owner and Billing Admin, with filters and manual retry action
Cap Utilization Reporting & Forecasting
"As an operations lead, I want visibility into cap utilization and runout forecasts across all retainers so that I can prioritize work and trigger extensions proactively."
Description

Provide portfolio-level dashboards and downloadable reports showing utilization by client, retainer, PO, and time period. Include trend charts, burn rate calculations, projected runout dates, and exception views (at-risk soon, paused for cap, awaiting extension). Support filters, saved views, scheduled email delivery, and API endpoints for BI tools. Normalize multi-currency views to a base currency and allow drill-down to underlying invoices, sessions, and expenses.

Acceptance Criteria
Portfolio Utilization Dashboard Aggregation
Given portfolio data exists across multiple clients, retainers, POs, and transactions within a selected date range When a user opens the Cap Utilization dashboard and selects a grouping of Client, Retainer, or PO and a time grain of Week or Month Then the dashboard displays per group: Cap Amount, Consumed Amount, Remaining Amount, and Utilization% = Consumed/Cap rounded to 1 decimal, plus a totals row Given the user applies filters (Client, Retainer, PO status) and a custom date range When filters are applied Then all metrics recompute and the dataset is limited to the filtered scope Given data volume up to 50,000 transaction line items When the dashboard loads Then the initial render completes within 3 seconds at the 95th percentile
Filters and Saved Views Persistence
Given a user composes filters, groupings, sorts, columns, and time range When the user saves the view with a unique name and selects "Set as default" (optional) Then the view is stored, appears in the view picker, can be set as org-default by admins, and loads with identical configuration Given a saved view is shared to org users When another user loads the view Then they see the same configuration but only data permitted by their permissions Given a saved view is edited and re-saved by its owner When scheduled deliveries or shared links reference that view Then future deliveries use the updated definition Given a saved view is deleted by an admin or the owner Then it is removed from the picker for all users and any schedules tied to it are disabled
Trend Charts, Burn Rate, and Projected Runout
Given consumption history exists for a PO or retainer within the selected range When the user enables Trend view with time grain Week or Month Then the chart displays cumulative Consumed vs Cap and per-period consumption; tooltips show period, consumed, cumulative, remaining, and utilization% Given the selected window spans at least 1 day When computing burn rate Then burn rate per day = total consumed in the last 30 calendar days divided by 30; if fewer than 30 days of activity exist, divide by the number of active days; display with 2 decimals Given Remaining > 0 and burn rate > 0 When computing projected runout Then Runout Date = today + ceil(Remaining / burn rate) days; if burn rate = 0 display "No burn"; if Remaining <= 0 display "Exceeded" Given new invoices, sessions, or expenses are recorded When data sync completes Then trend, burn rate, and runout recompute within 5 minutes
Exception Views: At-Risk Soon, Paused for Cap, Awaiting Extension
Given the Exceptions tab is selected When evaluating all active POs/retainers Then "At-Risk Soon" lists items with Utilization% >= 80% OR Projected Runout Date within 14 days; thresholds are configurable per org and the triggering reason is shown Given PO Cap Guard has paused auto-billing for an item due to cap reached When the Exceptions view loads Then the item appears under "Paused for Cap" showing paused timestamp and remaining amount (zero or negative) Given an extension request has been sent and awaits client action When the Exceptions view loads Then the item appears under "Awaiting Extension" with request date, requested amount, and a link to the request thread Given an admin updates exception thresholds When saved Then the lists refresh according to new rules within 5 minutes
Drill-Down to Invoices, Sessions, and Expenses
Given a user clicks a row or chart segment in the utilization dashboard When drilling down Then a transaction list opens scoped to the selected entity and time window, showing invoices, sessions, and expenses with columns: Date, Type, Description, Amount (base), Amount (original), Original Currency, PO/Retainer, Client, and Source Link Given role-based access controls restrict financial visibility When a user without finance privileges drills down Then monetary amounts are masked while counts and non-sensitive metadata remain visible; finance users see full details Given a user clicks a transaction ID in the drill-down When navigating Then the record opens in a new tab with breadcrumbs back to the dashboard and maintains filter context on return
Export and Scheduled Email Delivery
Given a user has a filtered or saved view loaded When exporting Then CSV and XLSX files are generated within 10 seconds for up to 100,000 rows and include visible columns plus a header block with export timestamp (UTC), base currency, and filter summary; exports respect current filters and sorts Given a saved view When the user schedules email delivery (daily/weekly/monthly, time-of-day, recipients) Then recipients receive an email at the scheduled time containing a secure link to the view and an attached CSV when the file is <= 10 MB; links expire in 7 days; failures are logged and an admin alert is generated Given an export or scheduled email is created When amounts are included Then all amounts are normalized to base currency with original currency and FX rate/date included as separate columns
BI API Endpoints and Multi-Currency Normalization
Given an API client with a valid OAuth2 token When calling utilization endpoints with filters (client_id, retainer_id, po_id, date_from, date_to, group_by, time_grain) and pagination Then the API responds 200 with cursor-based pagination, p95 response time < 1s for 10k records per page, and standard rate-limit headers; invalid params return 400 with actionable error messages Given amounts exist in multiple currencies When returning data via API and rendering in UI Then totals and derived metrics are normalized to the org base currency using end-of-day FX rates effective on each transaction date; fields include base_amount, original_amount, original_currency, fx_rate, and fx_rate_date; rounding to 2 decimals is applied consistently Given the org base currency is changed When dashboards reload or APIs are called Then normalized amounts recompute using the new base currency immediately, and a banner indicates the base currency and effective date Given an org timezone is set When grouping by day/week/month in UI or API Then period boundaries are computed using the org timezone consistently

Twin Clocks Overlay

Displays side‑by‑side local times for you and the client anywhere a time appears—booking cards, reminders, notes, and invoices. Hover reveals timezone codes and DST status; tap to flip the primary view. Eliminates “whose 3pm?” confusion and speeds confident confirmation.

Requirements

Automatic Timezone Resolution
"As a solo practitioner, I want SoloPilot to automatically determine and remember my and my clients’ time zones so that all times display correctly without me manually checking or converting."
Description

Determine and persist the correct time zone for both the workspace user and each client using multiple data sources in priority order (stored profile tz > connected calendar tz > booking-intake selection > device tz > IP geolocation fallback). Use IANA time zones and a DST-aware library to compute offsets, handle ambiguous/invalid local times around DST transitions, and display day-shift indicators when applicable. Keep user and client time zones synchronized across SoloPilot surfaces (booking cards, reminders, notes, invoices) and update gracefully when a user travels or a client’s zone changes, prompting for confirmation when uncertainty exists. All internal time calculations remain canonical in UTC with lossless conversion for consistent rendering and auditing.

Acceptance Criteria
Priority-Based Timezone Resolution for User and Client
Given a user or client record with both a stored profile time zone and a connected calendar time zone that conflict When timezone resolution runs Then the stored profile IANA time zone is selected and persisted unchanged Given no stored profile time zone but a connected calendar time zone exists When timezone resolution runs Then the connected calendar IANA time zone is selected and persisted Given no stored profile or calendar time zone but a booking‑intake time zone was selected When timezone resolution runs Then the booking‑intake IANA time zone is selected and persisted Given none of the above sources are available but the device time zone is available When timezone resolution runs Then the device IANA time zone is selected and marked as unconfirmed Given only IP geolocation is available When timezone resolution runs Then the IP‑derived IANA time zone is selected and marked as unconfirmed Rule: Only valid IANA time zone identifiers are accepted; non‑IANA strings are rejected with a validation error
Persist and Synchronize Resolved Timezones Across All Surfaces
Given resolved time zones for both user and client When viewing booking cards, reminders, notes, and invoices Then the same IANA time zones are used consistently to render all timestamps on every surface When a user updates their profile time zone and confirms the change Then all surfaces reflect the new time zone on next render without stale mixed zones When a client's stored time zone changes and is confirmed Then all future communications (reminders, invoices) render in the updated client time zone; past records remain unchanged Rule: Flipping the primary view in Twin Clocks does not modify stored time zones; it only affects display order
DST Transition Handling: Ambiguous and Invalid Local Times
Given a locale where a spring‑forward transition skips 02:00–02:59 When scheduling a session at a nonexistent local time in that zone Then the system prevents saving and suggests the next valid time with the computed UTC shown Given a fall‑back transition that repeats 01:00–01:59 When scheduling at an ambiguous local time Then the user must choose the first or second occurrence; the selected offset is stored with the UTC timestamp and renders consistently for both user and client Rule: Offsets and conversions are computed using a DST‑aware library against IANA time zones; test cases validate offsets on the DST boundary hours for at least 10 representative zones
Day‑Shift Indicator and Timezone Metadata Display
Given the Twin Clocks overlay is visible When hovering over either clock Then a tooltip shows the IANA zone (e.g., America/Los_Angeles), the current short code (e.g., PDT), and DST status (Active/Inactive) Given the user and client local dates differ When rendering the overlay Then a day‑shift indicator (+1 day or −1 day) is displayed next to the counterpart time When tapping the overlay to flip the primary view Then the metadata and day‑shift indicators remain accurate for the new ordering
Graceful Update on Travel or Timezone Change with Confirmation
Given a user's device time zone differs from the stored profile time zone by ≥30 minutes When SoloPilot detects the change Then a non‑blocking prompt asks to update the profile time zone; Accept updates and persists the new IANA zone; Decline maintains the current zone Given IP geolocation or device time zone suggests a different time zone and no higher‑priority source exists When detection occurs Then the time zone is not auto‑changed; a confirmation prompt is shown before updating Given a client selects a different time zone during booking intake When the booking is submitted Then the client's stored IANA time zone updates automatically and is marked confirmed; a change entry is recorded
Canonical UTC Storage and Lossless Conversion
Rule: All internal timestamps are stored in UTC with the associated IANA time zone identifier of the actor/context; no local‑time values are persisted for canonical fields Given a session created at 2025‑03‑09 10:00 America/Los_Angeles When saved Then the stored UTC equals the correct conversion for that zone on that date, and re‑rendering in America/Los_Angeles yields 10:00 Given any event timestamp and its stored UTC and IANA time zone When converting to local time for display Then the round‑trip (local -> UTC -> local) returns the original wall time across all supported surfaces Rule: Every time zone change (user or client) writes an audit record with old_tz, new_tz, source, actor_id, and UTC timestamp
Inline Twin Clocks Rendering
"As a consultant, I want to see my time and the client’s time side by side everywhere a time appears so that I can confirm scheduling at a glance and avoid “whose 3pm?” confusion."
Description

Render side-by-side local times for the user and the client adjacent to every timestamp on key surfaces (booking cards, reminders, notes, invoices, calendar views). Formatting is locale-aware (12/24h), includes short date as needed, and shows +1/−1 day badges when the date differs across zones. The component consumes a single canonical UTC timestamp plus two IANA zones and outputs a compact, responsive display that avoids overflow and respects existing typography. It degrades gracefully when a client time zone is unknown (show placeholder and quick-set action) and supports dark mode and print/PDF contexts without layout breakage.

Acceptance Criteria
Twin Clocks on Key Surfaces Without Overflow
Given a canonical UTC timestamp and two valid IANA time zones (user, client) When viewing booking cards, reminders, notes, invoices, and calendar views Then the component renders both local times side-by-side adjacent to every timestamp on those surfaces Given any surface listed above contains N timestamps When the page is rendered Then each timestamp has a corresponding twin clocks component with no missing instances Given viewport widths of 320px, 768px, and 1280px When the component is rendered Then the layout remains compact, avoids horizontal overflow, and stacks vertically at narrow widths while remaining side-by-side on wider viewports Given the host typography (font family, size, line-height) When the component is rendered Then it inherits typography without increasing line height by more than 0.25em and aligns baseline with surrounding text
Locale-Aware Time Format and Short Date
Given the user's locale preference is 12-hour time When rendering both times Then both times display in 12-hour format with am/pm per locale conventions Given the user's locale preference is 24-hour time When rendering both times Then both times display in 24-hour format with leading zeros per locale conventions Given the event's local calendar date differs between user and client zones or differs from today in either zone When rendering the component Then a short date (locale-aware) appears alongside each time and matches Intl.DateTimeFormat for the user's locale Given the event occurs on the same calendar date in both zones and is today in both zones When rendering Then the date is omitted to keep the display compact
Cross-Day +1/−1 Day Badges
Given the event falls on different calendar dates in the two zones When rendering Then a +1 or −1 badge appears next to the time(s) whose local date is ahead or behind the other zone Given both zones share the same local date for the event When rendering Then no +1/−1 badge is shown Given the short date is shown When badges are displayed Then badges do not duplicate the date information and remain visually compact (<= 3 characters)
UTC Conversion and DST Correctness
Given a canonical UTC timestamp and two IANA zones When converting to local times Then the component uses IANA rules to compute correct local times independent of device locale or clock Given the UTC timestamp falls during a DST transition for either zone When rendering Then the displayed local time reflects the correct offset for that instant, and no invalid/ambiguous local times are produced Given an invalid IANA zone string for either side When rendering Then that side shows a placeholder instead of a time and the component does not throw runtime errors Given the details tooltip is shown When rendering for any timestamp Then DST status (e.g., "DST in effect" or "Standard Time") is shown for each zone as of the event timestamp
Unknown Client Time Zone Placeholder and Quick-Set
Given the client time zone is unknown When rendering the component Then the user-side local time displays normally and the client-side displays a placeholder label Given the placeholder is activated by click or keyboard When the timezone selector is used to choose a valid IANA zone Then the component updates to show the correct client local time without a page reload and persists the selection for that client Given assistive technologies are used When navigating to the placeholder/action Then it is focusable, announces its purpose, and is operable via keyboard
Hover/Focus Details and Tap-to-Flip Primary View
Given a pointing device is used When hovering over the component Then a tooltip appears showing for each side the timezone code (e.g., PST/CEST), UTC offset, and whether DST is in effect at the event time Given keyboard navigation is used When the component receives focus Then the same tooltip content is available without requiring hover Given the component is tapped or clicked on the flip control When toggled Then the primary view flips (order/emphasis swaps between user and client), is visually apparent, and remains in effect for that instance until toggled again Given a small touch device When attempting to tap the flip control Then the tap target is at least 44x44 CSS pixels
Dark Mode and Print/PDF Support
Given system or app theme is dark When rendering the component Then text and badges meet WCAG AA contrast (≥4.5:1 for normal text), and no chromatic artifacts reduce legibility Given the page is printed or exported to PDF When rendering Then the layout does not wrap awkwardly or overflow, both times and any +1/−1 badges are fully visible in monochrome, and interactive affordances (flip control, tooltips) are suppressed Given a print preview at A4 and Letter paper sizes When rendering Then the component maintains alignment with surrounding text and does not cause pagination anomalies
Timezone Details Popover
"As a coach, I want quick access to precise timezone details (abbreviation, DST status, offset) so that I can confirm edge cases without leaving the page."
Description

Provide an on-hover (desktop) and tap/long-press (mobile) popover that reveals additional details: full time zone names, abbreviations, current UTC offsets, whether DST is currently in effect, and the next scheduled DST change. The popover anchors to the twin clocks component, is keyboard navigable, dismisses on Esc/blur, and is screen-reader friendly. Content is concise and localized, and the component reuses a shared popover framework to ensure consistency across SoloPilot.

Acceptance Criteria
Desktop Hover Popover
Given a desktop pointer device and the Twin Clocks component is visible When the user hovers the pointer over the Twin Clocks component for 150ms or more Then a timezone details popover opens within 200ms anchored to the Twin Clocks component And the popover remains open while the pointer is over either the trigger or the popover And the popover closes on Escape, on click/tap outside, or when the pointer leaves both trigger and popover for more than 100ms And re-hovering the trigger reopens the popover with current data
Mobile Tap and Long-Press Popover
Given a touch device (mobile/tablet) and the Twin Clocks component is visible When the user taps the Twin Clocks component Then the timezone details popover opens within 250ms anchored to the component And a subsequent tap on the trigger toggles the popover closed And a tap outside the popover or system Back (Android) closes the popover And a long-press of 300–500ms on the trigger also opens the popover if a tap is not registered as a click And the popover repositions on scroll to stay attached to the trigger until closed
Keyboard Navigation and Dismissal
Given the Twin Clocks component is reachable via keyboard Tab order When focus moves to the Twin Clocks component Then a visible focus indicator appears meeting WCAG contrast guidelines (3:1 minimum) When the user presses Enter or Space Then the timezone details popover opens and focus remains on the trigger And pressing Escape closes the popover and keeps focus on the trigger And moving focus away from both trigger and popover closes the popover And the trigger's aria-expanded reflects open/closed state appropriately
Screen Reader Accessibility
Given a screen reader is active (e.g., NVDA, JAWS, VoiceOver, TalkBack) When the popover opens Then the trigger has aria-haspopup set, aria-controls referencing the popover id, and aria-expanded reflecting state And the popover has an accessible role (tooltip or dialog) with an accessible name (e.g., "Timezone details") And the popover content is announced once in the user's locale without duplication and does not forcibly move focus And the user can dismiss with Escape or by moving focus, after which focus remains on or returns to the trigger
Timezone Content Accuracy and Localization
Given two participants with known time zones (one may observe DST, one may not) When the timezone details popover opens Then for each participant it displays: full time zone name (localized), abbreviation (e.g., PDT), current UTC offset (±HH:MM), DST status (In effect/Not in effect), and the next DST change with a localized date/time or "No upcoming DST" And all values match the authoritative IANA time zone database for the current timestamp And labels, dates, and number formats follow the app's selected locale (e.g., 12/24‑hour, month/day order) with a safe fallback to English if a translation is missing And content fits without horizontal scrolling; overflow is handled by wrapping or safe truncation without obscuring required fields
Anchoring, Placement, and Framework Consistency
Given varied viewport sizes (320px–1920px), LTR/RTL layouts, and scrollable containers When the popover opens Then it anchors to the Twin Clocks trigger and auto-positions to avoid viewport overflow (flips or shifts as needed) And it visually matches other SoloPilot popovers (theme, radius, shadow, animation) and is implemented using the shared Popover framework And it maintains correct layering above page content but below global modals And it repositions on window resize, scroll, or orientation change to keep alignment with the trigger
Live Updates and Edge Cases
Given the popover remains open across a DST boundary or minute tick When the underlying time zone causes a change in abbreviation, offset, DST status, or next change Then the displayed values update within 60 seconds without user action And if a time zone does not observe DST, show "DST: Not observed" and "Next DST change: None" And if a participant's time zone is unknown or invalid, show "Unknown time zone" for that participant and omit abbreviation and next-change fields for that participant And opening/closing the popover does not cause layout shift of surrounding content
Primary View Flip
"As a freelancer, I want to quickly switch the primary time between my local time and the client’s so that I can plan from whichever perspective I’m working in."
Description

Enable users to flip which time (mine vs. client’s) is emphasized as the primary display via tap/click on the twin clocks or a menu action. The preference persists per user and context (global default with per-surface override) and updates all visible timestamps instantly. Provide an optional keyboard shortcut and clearly indicate the active primary view while maintaining visual hierarchy and accessibility.

Acceptance Criteria
Flip Primary via Tap/Click
Given the Twin Clocks Overlay is visible on a supported surface When the user taps/clicks the twin clocks control Then the primary time toggles between "Mine" and "Client" And the active primary is visually emphasized and the inactive is deemphasized without loss of readability And a visible indicator updates to "Primary: Mine" or "Primary: Client" within 300ms of the interaction And the twin clocks update without a full page reload And hover tooltips continue to show timezone codes and current DST status for both clocks
Flip Primary via Menu Action
Given the overflow/context menu for the Twin Clocks is opened When the user selects "Set primary → Mine" or "Set primary → Client" Then the selected option becomes primary and the indicator updates within 300ms And the current primary option is marked as selected (e.g., aria-checked="true") and the other is not And the menu action is available on booking cards, reminders, notes, and invoices And the result is functionally identical to tapping/clicking the twin clocks control
Persist Global Default Per User
Given a user sets the primary and chooses "Set as my default" When the user reloads the app or signs in from another device/browser Then the chosen primary is applied by default on all surfaces without per-surface overrides And if no default has been set, the system default is "Mine" And the persisted default is stored per user account and loads within 1s of app start
Per-Surface Override and Precedence
Given a global default exists When the user sets a per-surface override (e.g., Notes = "Client") Then that surface shows "Client" as primary regardless of the global default And other surfaces continue to use the global default And clearing the override returns the surface to the global default And precedence is enforced: per-surface override > global default on navigation and reload And overrides persist per user across sessions
Instant Update of All Visible Timestamps
Given multiple timestamps are visible on the current surface When the primary is changed by any method (tap/click, menu, shortcut) Then all visible timestamps in the current view update to reflect the new primary within 300ms And no view shows mixed primary states during the update (single state per frame) And the time conversions are accurate to the minute, including across DST transitions for both user and client timezones
Keyboard Shortcut to Flip Primary
Given the Twin Clocks are visible or a time field is focused When the user presses Shift+P Then the primary flips and the indicator updates within 300ms And the shortcut is shown in a tooltip and the Help/Shortcuts modal And the shortcut does not trigger while typing in text inputs or when intercepted by OS/browser reserved shortcuts And the shortcut works on macOS, Windows, and Linux in the latest two versions of Chrome, Safari, Edge, and Firefox
Accessible Primary Indicator and Hierarchy
Given the primary is set Then the primary time has a clear visual hierarchy over the secondary while both meet WCAG 2.1 AA contrast (≥ 4.5:1 for text) And the twin clocks control is fully keyboard operable (Tab to focus; Enter/Space to toggle; Arrow/Enter to select menu options) And assistive technologies announce changes via an aria-live polite message within 500ms: "Primary time set to Mine" or "Primary time set to Client" And the menu options expose correct roles/states (role="menuitemradio" with aria-checked reflecting selection)
Overlay Preferences & Admin Controls
"As a SoloPilot user, I want control over how and where the twin clocks appear so that the interface matches my workflow and client mix."
Description

Add workspace and per-user settings to configure the Twin Clocks Overlay: enable/disable globally, choose default primary time, select 12/24-hour format, pick which surfaces show twin clocks, and define fallbacks when a client’s time zone is unknown. Allow per-client timezone overrides and a bulk update tool for client records. Provide a feature flag for gradual rollout and a help hint linking to documentation. Respect existing locale/i18n settings and audit timezone changes for compliance.

Acceptance Criteria
Feature Flag Gating and Preferences Visibility
Given the Twin Clocks feature flag is disabled for a workspace When any user visits Settings or any product surface Then no Twin Clocks preferences are shown and no twin clocks render anywhere Given the Twin Clocks feature flag is enabled for a workspace or cohort When a workspace admin opens Settings > Preferences Then a Twin Clocks Overlay section is visible with all configuration controls Given the feature flag is toggled from enabled to disabled When the user refreshes the app Then all twin clock UI is removed and times revert to single-clock display without layout regressions Given the feature flag is enabled When a user clicks the help hint in the Twin Clocks Overlay section Then a new tab opens to the Twin Clocks documentation URL and the request returns HTTP 200
Workspace Global Toggle Overrides User Settings
Given I am a workspace admin and set Enable Twin Clocks = Off at workspace level When any user views booking cards, reminders, notes, or invoices Then twin clocks are not displayed and user-level toggle is disabled Given Enable Twin Clocks = On at workspace level and a user disables Twin Clocks in their personal preferences When that user views any supported surface Then twin clocks are hidden for that user only Given Enable Twin Clocks = On at workspace level and a user has no personal preference set When they view supported surfaces Then twin clocks are displayed (workspace default applies)
Time Format Preference with Locale Fallback
Given no 12/24-hour format is set at user or workspace level When times are rendered in the Twin Clocks Overlay Then the format follows the user locale (e.g., en-US = 12h, en-GB = 24h) Given workspace default format is 24-hour and a user sets 12-hour in their personal preferences When that user views any supported surface Then both clocks display in 12-hour format for that user only Given a user changes their time format preference When they reload the app or return in a new session Then the selected format persists across sessions
Default Primary Time Selection and Flip Behavior
Given a user sets Default Primary Time = Client When they view booking cards, reminders, notes, or invoices Then the client time renders as the primary (prominent/first) clock on those surfaces Given Default Primary Time = Client and the user flips the primary view on a surface When they navigate away and return in the same session Then the flip state remains for that surface in-session but the saved default preference is unchanged Given the user signs out and back in When they view supported surfaces Then the saved default primary (Client or Me) is applied as the initial state
Surface Selection Controls for Twin Clocks
Given a workspace admin selects Booking Cards and Invoices only in Show On Surfaces When users view Reminders and Notes Then twin clocks are not displayed on Reminders or Notes Given the same configuration When users view Booking Cards and Invoices Then twin clocks are displayed on those surfaces Given a workspace admin updates the surfaces selection When a user reloads the affected pages Then the change is reflected within one page load
Unknown Timezone Fallback Behaviors
Given a client has no timezone and workspace fallback = Use workspace timezone When a user views that client on a supported surface Then the client clock shows using the workspace timezone and hover text indicates the assumption Given a client has no timezone and workspace fallback = Show only my time When a user views that client on a supported surface Then only the user’s local time is shown with an indicator that the client timezone is unknown and a link to edit the client record Given a client has no timezone and workspace fallback = UTC When a user views that client on a supported surface Then the client clock shows in UTC and is labeled as UTC Given a per-client timezone override exists When viewing the client Then the override takes precedence and fallback options are not applied
Per-Client Timezone Override and Bulk Update with Audit Trail
Given I have permission to edit clients When I set a timezone override on a client record Then all surfaces use the override immediately after save and an audit entry records user, timestamp, previous timezone, new timezone, and source = UI Given I upload a bulk timezone update file with client identifiers and valid IANA timezones in dry-run mode When validation completes Then I see a report of valid changes, invalid client IDs, and invalid timezones, and no changes are persisted Given I confirm the same bulk update to apply changes When processing completes Then valid client records are updated, audit entries are created with source = Bulk, and a downloadable results report lists successes and failures Given a bulk job exceeds 1,000 records When it runs Then it processes asynchronously with a visible progress indicator and final status summary in-app
Templates, PDFs, and Exports Support
"As a therapist, I want my confirmations, reminders, and invoices to show both my time and the client’s so that there’s no ambiguity outside the app."
Description

Extend notification templates (email/SMS), calendar invites, invoices, and exported reports to include twin times. Introduce template tokens for user-time and client-time with localized formatting, ensure PDF/print layouts don’t wrap or truncate, and include twin times in invoice line items and appointment summaries when applicable. For calendar invites, include both times in the description while preserving the event’s canonical UTC/zone definition to avoid double conversion by calendar apps.

Acceptance Criteria
Template Tokens for User and Client Local Times
Given an email or SMS template that includes the tokens {{event.user_time}} and {{event.client_time}} And the appointment has a stored start time in UTC and known user and client time zones and locales When the template is rendered for a specific appointment Then the output contains both user and client local times formatted per their respective locales (date language, month/day order, 12/24‑hour) And each rendered time includes the correct local time and time zone abbreviation (e.g., PDT, CET) And an optional format override parameter renders accordingly when specified (e.g., {{event.user_time format="EEE, MMM d, h:mm a z"}}) And if the client time zone is unknown, {{event.client_time}} renders as an empty string and no token artifacts remain in the output
PDF and Print Layout Integrity for Twin Times
Given an invoice or appointment summary PDF that includes twin times in headings and line items When the PDF is generated on A4 and US Letter with default margins and font sizes Then twin times appear on a single line without wrapping, truncation, or overlap in all sections And no column exceeds its allocated width, and all time strings are fully visible at 100% zoom and when printed And long time zone abbreviations or translated month names do not cause line breaks
Invoice Line Items Include Twin Times When Applicable
Given an invoice that contains line items linked to scheduled sessions with start times When the invoice is generated (HTML and PDF) Then each time-based line item displays both the user local time and the client local time adjacent to the session date And line items without a session datetime do not display twin times And the displayed times match the same session instant converted to each party’s time zone
Exports Provide Twin Times Columns
Given a user exports appointments or billing reports to CSV or XLSX When the export file is generated Then the dataset includes two separate columns: user_local_time and client_local_time in localized display format (including zone abbreviation) And the canonical event timestamp column remains unchanged And if the client time zone is unknown, client_local_time is empty
Calendar Invites: Twin Times in Description, Canonical Event Zone Preserved
Given a calendar invite (.ics) generated for an appointment When the .ics file is created Then VEVENT uses a single canonical time definition (DTSTART/DTEND) in the event’s configured time zone or UTC with VTIMEZONE as applicable And the DESCRIPTION field contains both user and client local times as plain text, clearly labeled, with zone abbreviations And importing the .ics into Google Calendar, Outlook, and Apple Calendar shows the event at the correct canonical time with the description unchanged and no duplicate/converted additional times added by the app
Localization: 12/24‑Hour and Language per Party Across Outputs
Given the user has locale preferences (e.g., en-US 12-hour) and the client has a different locale (e.g., fr-FR 24-hour) When generating emails, SMS, invoices (HTML/PDF), calendar invite descriptions, and exports Then user-time strings render in the user’s locale conventions and client-time strings render in the client’s locale conventions And numerals, month/day names, and 12/24-hour styles reflect each locale correctly And the same instant is represented consistently across all channels with only locale-specific formatting differences
Accessibility, Performance, and Telemetry
"As a product owner, I want the twin clocks to be accessible, fast, and measurable so that we deliver a reliable experience and can improve it with data."
Description

Meet WCAG 2.1 AA for the overlay: sufficient contrast, ARIA labels that read both times succinctly, logical tab order, and full keyboard operation for flip and popover. Set a performance budget of <5ms render per timestamp on modern devices via memoization/caching of zone offsets and string formatting, with batching to avoid layout thrash on lists. Instrument telemetry to track adoption, flip usage, unknown-timezone rates, DST-related warnings, and rendering errors; expose basic metrics in admin analytics and provide logs for support to troubleshoot timezone mismatches.

Acceptance Criteria
Screen Reader Announces Twin Times and DST
Given the Twin Clocks overlay is focused by a screen reader user When the element receives focus Then the accessible name/description announces exactly two times, two timezone codes, and the DST status for each without duplication And Given the flip control is focused When read by a screen reader Then its accessible name is "Flip primary/secondary time" And Given the info trigger is focused When read by a screen reader Then it has role "button" with accessible name "Timezone details" And When tested with NVDA (Windows) and VoiceOver (macOS) Then the announcement content and control roles are correctly exposed
Keyboard Operation for Flip and Popover
Given a keyboard-only user When tabbing through a timestamp with the overlay Then the tab order is: overlay container -> flip control -> info trigger -> next focusable element in document order When Enter or Space is pressed on the overlay container or flip control Then the primary and secondary times swap and the change is announced via aria-live polite When Enter or Space is pressed on the info trigger Then the timezone/DST popover opens with focus moved into it; pressing Esc closes the popover and returns focus to the trigger Then no arrow keys or non-standard keys are required to operate the overlay
Contrast and Focus Visibility
Given the overlay is displayed in any theme or state (default, hover, focus, active) Then normal-weight text contrast ratio is >= 4.5:1 against its background; large text >= 3:1; icons and focus indicators >= 3:1 And all hover/focus states maintain these minimum ratios And popover content (text, borders, icons) meets the same thresholds And no information is conveyed by color alone; an icon or text label accompanies color cues
Tab Order and Focus Management in Dynamic Lists
Given a virtualized list of 100+ timestamps When items mount/unmount during scroll Then focus is not lost or moved unexpectedly and the next Tab advances to the next visible overlay When the list re-renders due to data updates Then the element that had focus retains focus and scroll position is unchanged When flip is activated on any item Then focus remains on the activated control and no additional tab stops are introduced Then there are no tabindex > 0 values; tab order follows DOM order only
Per-Timestamp Render Budget and Batching
Given 100 timestamps rendered in a list When measured with the Performance API on modern devices Then per-timestamp cold render time is <= 5 ms at p95 and <= 2 ms at p95 on subsequent renders And rendering the list triggers <= 2 layout reflows and <= 2 style recalculations at p95 And scrolling through the list maintains >= 55 FPS on mid-tier devices And duplicate zone/time pairs in the same session are memoized such that the formatter is called no more than once per unique pair
Telemetry Events and Fields
When the overlay is first displayed in a session Then event "overlay_viewed" is emitted When the user flips times Then event "overlay_flip" is emitted with before/after primary indicators When client timezone cannot be resolved Then event "timezone_unknown" is emitted with attempted resolution path When a DST boundary warning is shown Then event "dst_warning" is emitted with boundary date When a render exception occurs Then event "overlay_render_error" is emitted with error code and stack fingerprint Then all events include tenantId, hashedUserId, sessionId, timestampId, userTZ, clientTZ, appVersion, and occurredAt (ISO-8601) And event delivery success rate is >= 99.5% over 24h and p99 ingestion latency <= 120 seconds And no PII (e.g., client names, notes text) is included in payloads
Admin Analytics and Support Logs
Given an admin user When visiting Analytics > Twin Clocks Then the dashboard displays daily active overlay users, flips per day, unknown timezone rate, DST warnings shown, and render error rate with date range filters (7/30/90 days) Then dashboard loads in <= 2 seconds at p75 and data freshness is <= 15 minutes Given a support agent user When searching Logs > Timezones by email, sessionId, or timestampId Then matching events are returned with userTZ, clientTZ, mismatch reason, and correlationId Then logs are retained for >= 30 days, exportable to CSV, and access is restricted to roles admin/support; unauthorized access returns 403

Respectful Hours Guard

Automatically blocks off-hour booking and message send times based on each party’s preferred windows. Suggests the nearest mutually respectful options and queues reminders to land at sane local hours. Reduces friction, boosts response rates, and protects your brand.

Requirements

Working Hours Profiles & Quiet Hours
"As a solo practitioner, I want to define my working and quiet hours (with exceptions) so that bookings and messages respect my availability."
Description

Configurable working hours, quiet hours, and exceptions per user and per contact, including per-day schedules, minimum/maximum lead times, service-specific windows, time buffers, and holiday calendars. Supports time zone selection with DST awareness, date-based exceptions, temporary overrides (e.g., travel), and blackout dates. Provides default templates for new contacts/services, import from connected calendars, and API fields for programmatic setup. Persists canonical settings used by SoloPilot scheduling and messaging components.

Acceptance Criteria
Per-User Working Hours With Time Zone and DST Persistence
Given a user selects time zone "America/Los_Angeles" and sets working hours Mon–Fri 09:00–17:00 local and quiet hours 17:00–09:00 When the user saves the profile Then the canonical settings persist with the selected time zone and per-day schedules And the stored UTC offsets reflect DST rules for each date And fetching the profile via UI and API returns the same local times for a sample date before DST and a sample date after DST And the settings are readable by scheduling and messaging components without transformation errors
Quiet Hours Enforcement for Outbound Messages
Given a contact has quiet hours 20:00–08:00 in time zone "Europe/Berlin" And the user composes a message at 21:30 contact-local time When the user clicks Send Then the system queues the message and sets send_at to the next allowed start of 08:00 local on the next calendar day that is not a blackout/holiday for the contact And the composer displays a confirmation notice with the scheduled send time in the contact’s local time And the event is logged with reason="quiet_hours" and the calculated send_at timestamp And attempting to force-send via API without override permission returns 403 with code="quiet_hours_blocked"
Mutual Respectful Booking Suggestion
Given the host has working hours 09:00–17:00 in "America/Los_Angeles" And the invitee has working hours 10:00–16:00 in "Australia/Sydney" And a 60-minute service is being scheduled When the invitee opens the scheduling link Then the availability grid only shows times that overlap both parties’ windows after applying time zones And if the invitee attempts to book a time outside either party’s window via direct URL parameters, the API responds 409 with code="outside_respectful_hours" And the UI suggests at least 3 nearest mutually respectful slots ranked by soonest time And the suggested slots respect configured buffers and lead times
Service-Specific Windows and Time Buffers
Given a service "Deep Dive" has service-specific availability Mon–Thu 10:00–15:00 and buffers of 15 minutes before and after And the host’s base working hours are broader than the service window When a client tries to book a 60-minute session starting at 09:50 or 15:00 local Then both attempts are rejected because buffers would violate the service window boundaries And consecutive bookings are only offered if the inter-slot gap is at least 15 minutes after buffer application And exported calendar events include buffer periods as busy so no overlapping bookings can occur
Minimum and Maximum Lead Time Enforcement
Given a service has min_lead_time=24 hours and max_lead_time=60 days When an invitee attempts to book 12 hours from "now" Then the booking is blocked with an inline message and the UI suggests the earliest allowable slot at now+24h aligned to the next valid window And the API returns 422 with code="min_lead_time_violation" and includes earliest_allowed timestamp When an invitee attempts to book 90 days from "now" Then the booking is blocked and the UI suggests the latest allowable slot within 60 days that is within valid windows And the API returns 422 with code="max_lead_time_violation" and includes latest_allowed timestamp And all calculations use the host’s time zone for lead-time windows
Exceptions, Holidays, Blackouts, and Temporary Overrides Precedence
Given the host’s base hours are Mon–Fri 09:00–17:00 in "America/New_York" And a holiday calendar marks 2025-12-25 as closed And a blackout date is set for 2025-10-31 (all day) And a date-based exception on 2025-10-31 allows 12:00–14:00 And a temporary override from 2025-11-01 to 2025-11-07 sets time zone to "Europe/London" with hours 08:00–12:00 When availability is generated for 2025-10-31, 2025-11-03, and 2025-12-25 Then 2025-10-31 shows only 12:00–14:00 available per exception despite the blackout And 2025-11-03 shows 08:00–12:00 in Europe/London per override, not the base hours And 2025-12-25 shows no availability regardless of base hours or buffers And after 2025-11-07 23:59:59 override expiry, availability reverts to base hours on 2025-11-08
Defaults, Calendar Import, and API Setup for Working/Quiet Hours
Given an admin defines a default contact template with time zone, working hours, quiet hours, and lead times When a new contact is created without explicit preferences Then the contact inherits the template values and the source is recorded as "template" When the user connects Google Calendar and enables import Then recognized Working Hours/Out-of-Office events adjust SoloPilot working hours and blackout dates respectively within the selected date range And imported changes are attributed with source="calendar_import" and are reversible via audit log When an integrator calls POST /api/v1/working-hours with a valid payload for a contact Then the API responds 201 with the canonical record and subsequent GET returns an identical representation And invalid time zone values return 422 with code="invalid_time_zone" And updates via PUT are idempotent And scheduling and messaging components reflect imported/API-updated settings within 60 seconds
Cross-Time-Zone Detection & Normalization
"As a coach with international clients, I want time zones handled automatically so that scheduling and messaging align with each person’s local hours without manual conversion."
Description

Automatically detects and maintains both parties’ time zones from profile settings, calendar metadata, booking link parameters, and geo/IP fallback, normalizing all windows and events to UTC for storage and computation. Handles DST transitions safely, recalculates when a party’s time zone changes, and displays times in each user’s local zone. Provides safeguards for ambiguous times, traveler mode prompts, and reliability monitoring. Integrates with SoloPilot calendar and messaging services.

Acceptance Criteria
Time Zone Source Detection and Precedence
Given a provider profile tz=America/New_York and provider calendar tz=America/Chicago, When detection runs, Then provider tz resolves to America/New_York with source=profile. Given a client opens a booking link with tz=America/Denver and has no profile tz, and IP geolocation resolves to Europe/Berlin, When detection runs, Then client tz resolves to America/Denver with source=booking_param. Given booking link tz is missing, client profile tz=Europe/Paris, calendar tz missing, and IP geolocation suggests Europe/Berlin, When detection runs, Then client tz resolves to Europe/Paris with source=profile. Given all explicit sources are missing and IP geolocation resolves to Asia/Tokyo, When detection runs, Then client tz resolves to Asia/Tokyo with source=ip_geo and confidence>=0.8. Rule: Detected time zones must be valid IANA identifiers; any non-IANA inputs are mapped to IANA before use. Rule: Detection completes within p95<=1s and p50<=200ms. Rule: All computed windows and events are stored in UTC with original tz recorded as metadata. Given a local time of 2025-03-30 09:00 Europe/Berlin, When stored, Then the UTC timestamp is 2025-03-30T07:00:00Z and metadata.tz=Europe/Berlin.
DST Transition Safety and Ambiguous Time Handling
Given a user schedules 2025-03-09 02:15 in America/Los_Angeles (nonexistent due to DST start), When they submit, Then the system rejects the invalid time, suggests 03:00 local, and allows confirmation of the adjusted time. Given a user schedules 2025-11-02 01:30 in America/New_York (ambiguous due to DST end), When they submit, Then the system requires selecting EDT (UTC-4) or EST (UTC-5) and stores the unambiguous UTC accordingly. Given reminders span a DST change, When reminders are generated, Then exactly one reminder is created per intended wall-clock time without duplicates or skips. Rule: All DST rules derive from the latest IANA tz database; updates are applied within 7 days of tzdb release.
Automatic Recalculation on Time Zone Change
Given a provider updates profile tz from America/New_York to Europe/London, When the change is saved, Then all future availability windows and respectful-hour computations are recalculated within 60 seconds. Given queued messages are targeted to land at the recipient's 09:00 local tomorrow, When the sender changes their own tz, Then the recipient delivery time remains at the recipient's 09:00 local. Given queued messages are constrained by the sender's respectful windows, When the sender tz changes, Then queued send times are recomputed to the new window and rescheduled. Then a summary notification lists the count of updated events/messages and any conflicts requiring review.
Traveler Mode Prompt and Temporary Override
Given the current IP-derived tz differs from profile tz by >=2 hours for >=15 minutes, When the user initiates a scheduling or messaging action, Then a Traveler Mode prompt offers a temporary override to the IP-derived tz. When the user accepts Traveler Mode for 14 days, Then suggestions and respectful windows use the temporary tz and a banner indicates Traveler Mode is active with a quick disable control. When the IP-derived tz matches the profile tz for 24 continuous hours or the override expires, Then Traveler Mode automatically turns off and tz reverts to profile. Rule: Declining the prompt suppresses it for 24 hours unless a new country is detected.
Localized Time Display for Each Party
Given a session stored as 2025-09-23T16:00:00Z, When viewed by a provider in America/Los_Angeles, Then the UI displays "Tue Sep 23, 9:00 AM (You • America/Los_Angeles, PDT)". When the same session is included in an email to a client in Europe/Berlin, Then the email displays "Tue Sep 23, 6:00 PM (Your local • Europe/Berlin, CEST)" and does not show the provider's local time. Rule: If both parties share the same tz, display a single localized time without duplication. Rule: Display uses IANA zone names and DST-aware abbreviations; numeric UTC offsets alone are not used for end-user labels.
Mutually Respectful Window Computation and Enforcement
Given provider window 09:00–17:00 America/Los_Angeles and client window 10:00–18:00 Europe/Berlin, When the client requests 07:00 Los Angeles time, Then the booking is blocked and nearest mutual slots within the next 7 days are suggested, showing both parties' local times. Given no mutual overlap exists within 14 days, When the client attempts booking, Then the system explains no overlap exists and offers the first next-available overlap date or an option to request temporary extension of hours. Given a reminder is created at 18:30 provider local, When queued for delivery to the client, Then it is scheduled to deliver at the client's next window start and the sender sees the computed delivery timestamp. Rule: Overlap computation normalizes to UTC and respects DST transitions for both parties.
Reliability Monitoring, Alerts, and Graceful Fallbacks
Rule: For each detection, record source, confidence, resolution time, and resulting tz; maintain p50 detection latency<=200ms, p95<=1s, and daily success rate>=99.5% excluding explicit permission denials. Given detection fails or yields an invalid/unknown tz, When an action requires a tz, Then fallback to IP-derived tz if available; else default to UTC and require explicit user confirmation before sending or booking. Given source disagreement >2 hours between highest-priority sources, When detection runs, Then prefer order [booking_param, profile, calendar, ip_geo], log a warning event, and show a non-blocking inline hint to review tz. Given error rate >1% for 5 consecutive minutes, When monitoring evaluates, Then alert on-call within 5 minutes and surface an admin status banner with current impact and mitigation steps. Rule: On IANA tzdb updates, run a background job to re-evaluate future events for impacted zones and notify users if any local wall-clock times change.
Off-Hours Booking Guardrail
"As a client booking a session, I want the system to prevent unreasonable times so that we avoid awkward or disruptive appointments."
Description

Enforces mutually respectful booking rules at the point of scheduling by validating requested slots against the provider’s working hours, the client’s allowed windows, service duration, buffers, and calendar conflicts. Blocks non-compliant times, explains the reason inline, and offers a controlled override flow requiring explicit confirmation and optional reason capture. Supports recurring bookings, rescheduling, waiting lists, and external booking page integrations via webhooks/API. Fully interoperates with SoloPilot’s scheduler and invoicing automations.

Acceptance Criteria
Mutual Windows and Time Zone Validation at Scheduling
Given a provider has working hours set to Mon–Fri 09:00–17:00 in America/New_York and a client has preferred windows set to Mon–Fri 08:00–18:00 in Europe/Berlin When the client attempts to book 17:30 America/New_York time for a 60-minute service Then the booking is blocked with reason "Outside provider working hours" and the UI displays the attempted start time in both parties' local times and time zones (IANA IDs), plus up to 3 nearest mutually respectful alternatives within the next 14 days Given the same provider and client settings When the client attempts to book 10:00 America/New_York that is 16:00 Europe/Berlin but their preferred window for that weekday ends at 15:00 Then the booking is blocked with reason "Outside client preferred hours" with dual-time display and suggestions as above Given a requested slot falls within both the provider's hours and the client's preferred window (accounting for DST in each locale) When the client submits the booking Then the booking passes this validation and proceeds to subsequent checks
Duration, Buffer, and Service Rules Enforcement
Given a service duration of 60 minutes with a 15-minute post-buffer and 0-minute pre-buffer and the provider's day ends at 17:00 local time When the client selects a 16:15 start Then the booking is blocked with reason "Exceeds working window with buffer" and suggestions start at the next slot that keeps service+buffer within working hours Given a 60-minute service with a 15-minute post-buffer and an existing appointment ending at 14:00 (with no buffer) When the client selects a 13:15 start Then the booking is blocked with reason "Insufficient buffer between appointments" and the earliest suggested alternative starts at 14:15 or later Given a service with configured buffers When a client selects a slot that satisfies duration and buffer constraints Then this validation passes
Conflict Detection Across Calendars
Given the provider has SoloPilot calendar events and an external Google Calendar connected with 5-minute sync freshness When a client selects a slot that overlaps any busy block on either calendar Then the booking is blocked with reason "Calendar conflict" and the inline message identifies the source (e.g., "Google Calendar: Team Standup") and shows the last sync timestamp Given no overlapping busy blocks across connected calendars When the client attempts to book Then the conflict validation passes
Inline Blocking Feedback, Suggestions, and Waiting List Option
Given a client selects any non-compliant slot (outside hours/windows, conflicts, or buffer violations) When validation fails Then the UI shows an inline, human-readable reason and a machine-readable reason code, plus up to 3 nearest mutually respectful alternatives within 14 days; if none are available within 30 days, the UI offers a "Join waiting list" action capturing preferred days/times and contact method Given the client chooses "Join waiting list" When they submit preferences Then a waitlist entry is created, the client receives a confirmation, and the provider sees the waitlist entry with the requested time window
Controlled Override With Confirmation and Audit
Given a booking attempt violates provider working hours When initiated by a client Then the override option is not offered to the client; the booking remains blocked with explanation Given a booking attempt violates provider working hours When initiated by the provider (or an authorized admin) Then an override modal appears requiring explicit confirmation and optional reason (0–500 chars); upon confirmation, the booking is created with override=true, the reason text, actor identity, and timestamp recorded in an immutable audit log, and a visible "Override" badge is shown on the event Given a booking attempt violates only the client's preferred window (not provider hours) When initiated by the client Then a controlled override is permitted with explicit confirmation and optional reason capture, and the booking is created with override=true and audit record as above
Recurring and Reschedule Validation
Given a weekly recurring booking request for 10 occurrences When some occurrences violate mutually respectful windows or conflict with calendars Then the system presents a per-occurrence validation summary, offers per-occurrence suggestions to move to the nearest compliant time, and allows the requester to skip or adjust those occurrences; only compliant or adjusted occurrences are created, and a final summary of created/moved/skipped is shown Given an existing booking that is being rescheduled When the new time is selected Then all validations (windows, buffers, conflicts) are re-applied; if the original event had an override for a specific rule, the reschedule requires explicit reconfirmation if the same rule is still violated and creates a new audit entry
External Booking/Webhook Integration and Automation Interop
Given an external booking page submits a booking via API with an Idempotency-Key header When the requested slot is non-compliant Then the API responds 409 Conflict with a machine-readable error code (e.g., OUTSIDE_PROVIDER_HOURS, OUTSIDE_CLIENT_WINDOW, CONFLICT, INSUFFICIENT_BUFFER) and a suggestions array (0–3 ISO-8601 datetimes with TZ) Given the same request is retried with the same Idempotency-Key When processed Then only one booking is created and the same response is returned Given a compliant booking is created via API or UI (with or without override) When the booking is confirmed Then SoloPilot scheduler automation triggers normally and invoicing automation prepares the appropriate draft invoice per service rules, with the booking payload including override flag and reason (if any), and outbound webhooks are fired with the final status
Mutual Window Suggestion Engine
"As a therapist, I want instant alternative suggestions when a slot is unavailable so that scheduling completes quickly without back-and-forth."
Description

Computes and ranks the nearest mutually acceptable time slots when a requested time is blocked, considering preparation buffers, lead-time constraints, provider preferences (e.g., start-of-day bias), and recipient quiet hours across time zones. Returns the top N suggestions with localized labels and clear explanations, supports quick-accept actions, and gracefully expands the search across days while respecting holidays and blackout dates. Exposes suggestions via API and UI on SoloPilot booking flows.

Acceptance Criteria
Top N Ranked Mutually Respectful Slots for Blocked Request
Given a booking request at requested_time that violates at least one constraint When the engine computes suggestions with topN = N Then it returns up to N distinct time slots that satisfy all mutual constraints (quiet hours, buffers, lead time, blackouts, holidays) And the suggestions are ordered by descending score, with ties broken by earliest chronological start And each suggestion includes an explanation array with at least one machine-readable reason and a human-readable summary referencing the violated constraint And each suggestion includes the absolute time distance from requested_time in minutes
Time Zone and Quiet Hours Compliance with Localized Labels
Given provider_tz and client_tz are set and quiet hours are configured for both parties When suggestions are generated Then no suggestion starts or ends outside either party’s allowed windows in their own time zone And daylight saving offsets are respected using IANA time zones for all computed slots And each suggestion includes provider_local_label and client_local_label formatted per the viewer’s locale and shows the correct zone abbreviation
Lead-Time and Preparation Buffer Enforcement
Given lead_time_hours = L and prep_buffer_before = B1 and prep_buffer_after = B2 When suggestions are generated at now = T0 Then all suggestion start times are greater than or equal to T0 + L hours And no suggestion overlaps existing bookings when expanded by buffers B1 and B2 And if the requested time is rejected solely due to buffer conflict, at least one suggestion is the nearest time after the buffer clears And explanations include buffer_conflict and/or lead_time when those constraints cause rejection
Provider Preference Biasing (Start-of-Day) in Ranking
Given provider sets start_of_day = S, bias_window = 2h, and bias_weight > 0 When two candidate slots are equidistant from requested_time, one within [S, S+2h) and one outside Then the in-window slot ranks higher And each suggestion includes a numeric score and an integer rank starting at 1 with no gaps And explanations include preference_bias for slots whose rank improved due to the bias
Graceful Cross-Day Expansion Respecting Holidays and Blackouts
Given no valid slots exist on the requested date When the engine expands search with dayExpansionLimit = D (default 7) Then it searches sequentially day-by-day up to D days and excludes provider holidays and blackout dates And the earliest valid slot across the expanded range appears before later-day options with lower proximity And the response indicates searchWindowExpanded = true and daysSearched equals the number of distinct dates evaluated
Quick-Accept Workflow from Suggestions
Given a suggestion is returned with quick-accept enabled and capacity is still available When the recipient invokes quick-accept within token_ttl Then a booking is created for that slot with buffers applied, a confirmation is sent, and the suggestion becomes unavailable for subsequent quick-accept And if the slot becomes unavailable before accept, the action returns 409 Conflict and prompts regeneration of suggestions And expired quick-accept tokens return 410 Gone and do not create a booking
Suggestions API Contract and Performance
Given a GET /v1/suggestions request with parameters (requested_time, topN, provider_id, client_id) When the request succeeds Then the response is 200 with suggestions[], each containing id, start, end (ISO 8601), provider_tz, client_tz, provider_local_label, client_local_label, score, rank, explanations[], distance_minutes, and a quick_accept_token or quick_accept_url And if no suggestions exist, the response is 200 with an empty suggestions[] and meta.reason = no_available_slots And p95 latency is ≤ 400 ms for topN ≤ 10 and a search window ≤ 30 days with up to 200 calendar events
Respectful Messaging Scheduler
"As a freelancer, I want my messages to arrive at reasonable times for clients so that they’re likelier to read and respond."
Description

Queues automated reminders, follow-ups, and invoices to deliver within each recipient’s acceptable hours and preferred channels, adjusting for weekends, holidays, and DST. Applies channel-specific constraints (e.g., SMS quiet hours/local regulations), defers sends that would land at off hours, and retries intelligently. Provides “send now” override with confirmation, idempotent delivery, and per-contact quiet hours that cascade from workspace defaults. Integrates with SoloPilot automations, invoicing, and notes workflows.

Acceptance Criteria
Quiet Hours Cascade to Contacts
Given workspace quiet hours are 20:00–08:00 Mon–Fri and all day Sun in the contact’s local timezone and the contact has no override When an automation schedules a reminder at 21:30 local time on Tuesday Then the message is queued for 08:00 Wednesday local time Given the contact defines quiet hours 19:00–07:00 daily When an automation schedules any message for 06:50 local time Then it is deferred to 07:00 local time Given workspace timezone is America/New_York and contact timezone is Europe/Berlin When the engine evaluates the allowed window for that contact Then it uses Europe/Berlin local time for scheduling
DST and Holiday-Aware Scheduling
Given recipient timezone is America/Los_Angeles and a message is targeted for 07:30 local time on the day DST starts When clocks advance at 02:00 Then the message sends at 07:30 PDT Given a recipient holiday 2025-11-27 is marked Do not send When a message would otherwise send that day Then it is deferred to the next business day at the start of the allowed window Given a message is queued for 07:30 local time on the day DST ends When clocks fall back at 02:00 Then the message delivers once at 07:30 local time and is not duplicated
Channel Preferences and Regulatory Constraints
Given a contact’s preferred channels are ordered SMS then Email and SMS quiet hours are 21:00–08:00 local due to regulation When an automation attempts to send at 22:15 Then the system evaluates Email and sends via Email if within allowed hours otherwise defers Given SMS to the recipient’s country is prohibited on Sundays When an automation would send on Sunday Then SMS is suppressed and the next compliant channel is used or the message is deferred Given a channel is disabled for the contact When selecting a channel for delivery Then the disabled channel is never used Given a contact has no preferred channels configured When selecting a channel Then the workspace default channel order is used
Deferred Send and Intelligent Retry
Given a message is scheduled for 06:55 and the allowed window starts at 07:00 local time When the scheduler evaluates the send time Then the send time is set to 07:00 local time Given a transient provider error such as HTTP 429 or 5xx occurs When sending a message Then the system retries up to 3 times with exponential backoff delays of 2 minutes then 4 minutes then 8 minutes within the allowed window Given the allowed window closes before the next retry When the backoff elapses Then retries pause and resume at the next allowed window start Given a permanent error such as non retryable 4xx occurs When sending a message Then the message is marked Failed no further retries occur and the owner is notified Given all retries are exhausted without a successful send When evaluation completes Then the message status is Failed and an audit log entry is written
Send Now Override with Confirmation and Audit
Given a queued message is currently outside allowed hours When a user with role Owner or Admin clicks Send Now Then a confirmation modal explains off hour delivery and requires typing SEND NOW to proceed Given the user confirms and the channel has no hard legal prohibition When Send Now is executed Then the message sends immediately bypassing quiet hours and an audit log records user timestamp justification and message ID Given the channel has a hard legal prohibition in the recipient’s locale When the user attempts Send Now Then the system blocks the action and displays the reason Given the user cancels the confirmation modal When Send Now is not confirmed Then no message is sent and the original schedule remains
Idempotent Delivery Across Retries and Triggers
Given an automation fires twice for the same session and contact within 24 hours producing the same logical message When both attempts use the same idempotency key Then only one delivery occurs and the duplicate attempt is recorded as Already Delivered Given a retry is attempted for a previously delivered message When the same idempotency key is used Then the provider is not called again if the prior attempt was acknowledged delivered and the attempt is logged as Already Delivered Given an invoice reminder is regenerated before scheduled send When content updates occur Then exactly one message is sent at delivery using the latest content and the idempotency key remains consistent across retries
Automation and Workflow Integration
Given a SoloPilot automation Session Completed generates a follow up and an invoice When the event occurs outside the recipient’s allowed hours Then both messages are queued for the next allowed window per contact Given invoice notes are edited before the scheduled send time When the send occurs Then the message renders the latest invoice and notes content at send time Given a scheduled message is manually rescheduled by a user When the new time violates allowed hours Then the system adjusts to the nearest allowed time and displays the adjusted time to the user Given a message of the same type for the same trigger and contact already sent within 24 hours When the automation attempts to schedule another Then no new schedule is created and the event is logged as Suppressed Duplicate
Global Defaults & Override Controls
"As an independent professional, I want sensible defaults and controlled overrides so that setup is fast and exceptions are deliberate."
Description

Workspace-level defaults to auto-apply respectful hours to new contacts and services, with bulk update tools and segment-based policies (e.g., by locale or service type). Offers granular override permissions, warnings when overrides violate policy, and audit prompts capturing reason and duration. Supports assistant access, import/export of settings, and conflict detection between global defaults and individual preferences. Ensures consistent behavior across SoloPilot scheduling and messaging.

Acceptance Criteria
Auto-Apply Global Defaults to New Entities
Given a workspace has Respectful Hours global defaults configured and auto-apply enabled When a new contact is created via UI, import, or API Then the contact inherits the appropriate default (segment-matched if applicable, else base workspace default) Given a workspace has service-level defaults configured When a new service is created Then the service inherits the workspace or segment default respectful hours Given inherited respectful hours exist When a booking or message is initiated with a new contact Then scheduling availability and message send windows enforce the inherited defaults in both modules
Configure and Inherit Segment-Based Policies
Given an admin creates or edits a segment policy (e.g., by locale or service type) When the policy is saved Then all matching contacts/services inherit the segment’s respectful hours unless they have explicit overrides Given multiple segment policies could match an entity When resolving effective respectful hours Then precedence is: individual override > most-specific segment > workspace default Given an entity’s attributes change so segment membership changes When the entity is saved Then the effective respectful hours recalculate based on the new segment membership and are visible in the entity’s settings
Bulk Update Propagation with Preview
Given a workspace default or segment policy is modified When an admin opens Bulk Update Then a preview shows total entities eligible, breakdown by type, and counts excluded due to individual overrides Given the bulk update preview is accepted When the update is executed Then the system applies changes only to eligible entities and returns counts of updated, skipped, and errored items Given bulk updates have been applied When users schedule or compose messages subsequently Then future availability and send windows reflect the new policies; existing booked sessions and already-queued messages remain unchanged
Role-Based Override Controls Including Assistants
Given role permissions are defined for Owner, Admin, and Assistant When a user attempts to create or edit an override on an entity Then only users with override permission for that entity type can proceed; assistants can act on behalf of the owner within assigned workspaces Given an override is being created When the user saves the override Then reason and duration (start and end) are required fields; save is blocked if missing Given an assistant performs an override change When the change is saved Then the audit log records the acting assistant, delegating owner, timestamp, and affected entity
Policy Violation Warning and Audit Capture
Given an action would violate current respectful hours (booking or message send time) When a user attempts to save/send Then the UI displays a violation warning and shows the nearest respectful options; users without override permission cannot proceed outside policy Given a user with permission chooses to override a violation When the override is confirmed Then the system requires reason and duration, logs an audit event, and labels the affected item as override-applied Given an override expires When the expiration time passes Then the entity automatically reverts to its prior effective policy and the audit trail records the reversion
Conflict Detection and Resolution Across Layers
Given global defaults, segment policies, and individual preferences may differ When computing allowed booking and send windows Then the system detects conflicts, applies the defined precedence, and displays the effective policy and its sources to the user Given a user is in Scheduling or Messaging When viewing available times or send-time suggestions Then the same effective respectful hours are enforced in both modules, with suggestions shown in each party’s local time for the nearest mutually respectful options Given a policy change occurs at any layer When the user revisits the entity or refreshes Then the effective policy recalculates and UI indicators reflect the updated source and time of change
Import/Export of Respectful Hours Settings
Given an admin requests export of respectful hours settings When the export runs Then a portable file is generated containing workspace defaults, segment policies, and overrides with schema version and counts Given an admin uploads a valid import file When running a dry-run Then a report shows intended creates/updates/deletes and validation results without applying changes Given the admin confirms a valid import When the import executes Then changes are applied, a summary of applied/skipped/errors is presented, and only users with appropriate permissions can perform the import Given an import file is invalid or the user lacks permission When the import is attempted Then the system rejects the import with specific errors and no settings are changed
Insight Dashboard & Audit Log
"As a consultant, I want visibility into blocks and outcomes so that I can tune settings and prove professionalism to clients."
Description

Centralizes analytics and event logs for blocked bookings, deferred messages, overrides, and suggested-slot accept rates. Surfaces trends (e.g., top off-hour attempts, best-performing send windows), estimates time saved, and highlights misconfigurations. Provides searchable audit trails with timestamps, actors, reasons, and before/after times, plus export/retention controls and privacy redaction options. Aligns metrics with SoloPilot’s billing and scheduling KPIs to demonstrate impact.

Acceptance Criteria
Metrics and Trends for Blocked and Deferred Events
Given I am an org admin viewing the Insight Dashboard for a selected date range When the dashboard loads Then it displays total counts for blocked booking attempts, deferred message schedules, overrides, suggestions sent, and suggestions accepted within the selected range And the dashboard shows daily trend charts for each metric with UTC-normalized timestamps and an org-local timezone toggle And I can filter metrics by client segment, service type, staff/owner, and channel (email/SMS), and filtered totals update within 1 second after a filter change And the dashboard lists the top 5 off-hour attempt windows by frequency and the top 5 send windows by response rate for the selected range And metric tiles include previous-period deltas (+/− %) and values match backend aggregates within 0.5% absolute difference And p90 initial dashboard render completes within 3 seconds for up to 100k events in range
Suggested-Slot Acceptance Rate Computation
Given suggestions generated by Respectful Hours Guard in the selected date range When acceptance rate is calculated Then acceptance rate = accepted suggestions / suggestions sent, excluding suggestions withdrawn before delivery and deduplicated retries And the rate is shown overall and broken down by channel (email/SMS) and time-of-day buckets And the rate updates correctly when filters (date range, service, staff, client segment) change And zero-denominator cases display as N/A without errors And displayed rates match backend recomputation within 0.5% absolute difference
Audit Log Completeness and Search
Given blocked bookings, deferred messages, overrides, suggestions sent, and suggestions accepted occur When these events are written to the audit log Then each record contains ISO-8601 timestamp with timezone, actor (user id or system), client id, event type, reason code and human-readable reason, before/after local times, and correlation id And new events appear in the log within 60 seconds of occurrence And I can search by keyword and filter by event type, actor, client, and date range, and sort by timestamp And results are paginated at 50 per page with a total count displayed And p90 search latency is under 2 seconds for queries over up to 500k records And clicking any metric on the dashboard deep-links to a pre-filtered audit log view matching the metric and date range
Privacy Redaction Controls and Access
Given organization-wide redaction is enabled When viewing the Insight Dashboard and Audit Log Then PII fields (client name, email, phone, and free-text message content) are masked while time fields and reason codes remain visible And exports generated by non-privileged roles reflect redaction And privileged roles (Owner, Compliance) can view unredacted data; access is gated by permission and all reveals are audited And changes to redaction settings and unredacted export actions create audit log entries with actor, timestamp, and scope And enabling or disabling redaction does not change aggregate metrics or computed rates
Export and Retention Governance
Given I have Export Logs permission When I export audit logs or metrics for a selected date range Then I can choose CSV or JSON and optionally include PII if I have permission And the export contains displayed fields plus query metadata (filters, time range, generated at) And large exports (>100k rows) are delivered as chunked ZIP archives with p95 completion under 5 minutes for up to 1M rows via background job And redaction settings are applied unless PII inclusion is explicitly selected by an authorized user And an export record (actor, time, filters, row count, PII flag) is written to the audit log Given retention is configured to N days (30–365) When the retention job runs daily Then records older than N days are permanently deleted and a summary deletion log entry is created And deleted records are not retrievable via UI or export
Misconfiguration Detection and Remediation Hints
Given organization settings and recent Respectful Hours Guard events are available When misconfiguration rules are evaluated hourly Then the dashboard surfaces issues including: no overlap between staff and client preferred windows, missing client timezone, Respectful Hours Guard disabled on active services, override rate > 20% over last 14 days, suggestion rejection/expiry rate > 60% over last 14 days And each issue includes severity (High/Medium/Low), affected count, and a Fix link to the relevant settings page And clicking an issue opens a pre-filtered audit log showing supporting events And resolving a misconfiguration removes the issue on the next data refresh (within 15 minutes)
Time Saved and KPI Impact Estimation
Given default time-saved assumptions are set (e.g., 3 minutes per blocked booking, 2 minutes per deferred message) And the organization hourly rate is configured in SoloPilot billing settings When the dashboard calculates time saved for the selected range Then total minutes saved = sum of (event count × per-event minutes) and estimated value saved = (minutes/60) × hourly rate And displayed totals reconcile with underlying event counts and the assumptions table And users with permission can edit the per-event minute assumptions; changes are versioned and audited And if no hourly rate is set, the dashboard shows minutes saved without currency values And the panel displays aligned KPIs (booking conversion within respectful windows, average reply lag) with definitions and values matching SoloPilot KPI reports within 1% absolute difference And time-saved charts render within 3 seconds at p90 for up to 100k events

DST Guardian

Monitors upcoming daylight‑saving shifts for both parties and keeps recurring appointments pinned to each person’s intended local time. Sends proactive heads‑up notices with one‑tap rebase for all affected sessions. Prevents accidental early/late arrivals and missed sessions.

Requirements

Dual-Party DST Monitoring
"As a solo practitioner, I want SoloPilot to automatically detect and monitor DST changes for me and my clients so that my schedule stays accurate without manual checks."
Description

Continuously detect and track time zones and upcoming daylight-saving transitions for both the host and each client using IANA tzdata. Persist per-contact time zone, inferred from user profile, connected calendars, and explicit user selection with IP-based fallback. Maintain awareness of future offset changes for 12–18 months and compute their impact on all scheduled and recurring sessions. Automatically refresh when tzdata updates occur and when users travel or connect new calendars. Provide a reliable data layer that other modules (scheduling, notifications, automations, invoicing) can query to determine if and when a session’s local time will shift due to DST, ensuring accuracy without manual checks.

Acceptance Criteria
Persist and Resolve Per-Contact Time Zone with Source Precedence
Given a contact has multiple time zone signals (explicit selection, connected calendar, user profile, IP) When resolving the contact’s time zone Then the system selects the highest-precedence valid IANA zone using: explicit selection > connected calendar primary > user profile > IP fallback And when any higher‑precedence source changes, then the stored time zone updates within 60 seconds and persists source, confidence, tzdata_version, and updated_at And if a provided zone is invalid or not IANA, then it is rejected and the next source is evaluated And if no valid source is available, then timezone_status=unknown is stored and exposed via the query API And all changes create an audit record with prior_zone, new_zone, source, and timestamp
18‑Month DST Transition Forecast Cache
Given a contact’s time zone is set and a tzdata version is active When initializing or refreshing transitions Then the system precomputes and caches all offset transition instants for the next 18 months for that zone And the cache includes fields: effective_at_utc, from_offset_minutes, to_offset_minutes, tzdata_version And when tzdata updates are detected, then all transition caches are rebuilt and marked with the new tzdata_version within 24 hours And per‑contact transition computation completes in ≤100 ms on average over 1,000 contacts
Dual‑Party Session Impact Computation for Recurring Series
Given a recurring series with an RRULE and a base start time When generating occurrences across an 18‑month horizon Then for each occurrence the system computes host_local_time, client_local_time, host_offset, client_offset using current tzdata And the system flags an occurrence shift for a party when the local wall time deviates from the party’s original series local time, recording shift_delta_minutes and shift_reason=dst_transition And the system returns an impact list including occurrence_id and per‑party shift flags/deltas And generating 1,000 occurrences with impact computation completes in ≤1,000 ms on a standard worker
Real‑Time Recalculation on Travel or Calendar Connect
Given a host or client connects a new calendar with a different time zone or travels (device reports a new primary zone) When the change event is received Then the system reevaluates the contact’s time zone using precedence rules and updates persisted data within 2 minutes And all affected sessions and series have their occurrence computations recalculated and caches refreshed within 2 minutes And the query API begins returning updated local times and shift flags within the same 2‑minute window
Query API for Session DST Shift Status
Given a consumer requests DST impact for a session occurrence or series When calling GET /dst-impact with session_id or series_id and optional range Then the API returns for each occurrence: host_zone_id, client_zone_id, host_local_start, client_local_start, host_offset, client_offset, host_shift_flag, client_shift_flag, shift_delta_minutes (per party), next_shift_at (per party), tzdata_version And responses are paginated, ordered by start_utc, and filterable by start/end range And the endpoint is idempotent and responds in ≤300 ms for a single session or ≤1,000 ms for up to 500 occurrences And when a party’s time zone is unknown, the API sets that party’s shift fields to null and timezone_status=unknown
Handling Ambiguous or Invalid Time Zone Inputs
Given time zone inputs from external sources may be ambiguous or non‑IANA (e.g., Windows IDs, Etc/GMT) When resolving to an IANA zone Then the system maps via a canonical mapping table; if unmappable, it falls back to the next source in precedence And when IP inference yields multiple candidates, the system selects the candidate matching current UTC offset at query time; if still ambiguous, timezone_status=unknown is set And all ambiguities, fallbacks, and unknown outcomes are recorded in audit logs and exposed via API fields
Tzdata Update Auto‑Refresh and Regression Guard
Given a tzdata release upgrade is applied (e.g., 2025a→2025b) When the new tzdata becomes active Then all transition caches and session impact computations are recomputed using the new version And a regression guard verifies that occurrences in zones without rule changes have zero change in computed local times; if >0.1% differ, the update is flagged as failed And the system publishes active tzdata_version, recomputation completes within 24 hours, and query API responses reflect the new version
Local-Time Pinning for Recurring Series
"As a coach who runs weekly sessions across time zones, I want recurring appointments to stay pinned to each person’s intended local time so that nobody shows up early or late after DST switches."
Description

Introduce a scheduling model that stores each series’ intended local time and pinning strategy (host-pinned, client-pinned, or dual-pin with precedence). On render, recompute each occurrence’s UTC time across DST boundaries so that each participant experiences the session at the intended local clock time. Support cross-time-zone pairs where DST rules differ, per-occurrence exceptions, and mid-series participant time-zone changes. Show a clear UI indicator when an upcoming DST shift will alter the computed UTC time while keeping local times stable per the chosen pin rule. Ensure compatibility with buffers, availability windows, and no-overlap constraints.

Acceptance Criteria
Host-pinned weekly series across US DST end
Given a weekly series with host timezone America/New_York and pinning strategy = host at 10:00 host local And the series spans the US DST end transition When rendering the occurrence one week before the transition Then the host sees 10:00 host local and the computed UTC time is 14:00 (UTC-04:00) When rendering the occurrence one week after the transition Then the host sees 10:00 host local and the computed UTC time is 15:00 (UTC-05:00) And ICS exports and reminders use these recomputed UTC values And no change-log entry is created for DST-driven UTC adjustments
Client-pinned series; client DST starts; host has no DST
Given a weekly series with client timezone Europe/London and pinning strategy = client at 14:00 client local And host timezone = Asia/Kolkata (no DST) When the client's DST starts (Europe/London offset changes from +00:00 to +01:00) Then the client still sees 14:00 local And the computed UTC changes from 14:00Z to 13:00Z And the host sees the occurrence shift from 19:30 to 18:30 host local And capacity, buffers, and no-overlap checks are evaluated using the host-local 18:30 start
Dual-pin with host precedence during DST mismatch
Given a dual-pin series with host timezone America/New_York and client timezone Europe/London And intended local times host = 09:00, client = 14:00 with precedence = host When rendering an occurrence during a week when US DST is active and UK DST is not Then the host sees 09:00 host local as intended And the client sees 13:00 client local (shifted -1h from intended due to precedence) And the UI notes the shift for the client (e.g., "shifted by -1h due to precedence") And the meeting's UTC equals the instant corresponding to 09:00 America/New_York on that date
Mid-series client timezone change on client-pinned series
Given a weekly series pinned = client at 10:00 client local And client timezone initially = America/New_York; host timezone = America/Denver When the client updates their profile timezone to America/Chicago effective 2025-03-15 Then all occurrences with start >= 2025-03-15 render at 10:00 America/Chicago for the client And earlier occurrences remain at 10:00 America/New_York And host-local times and computed UTC values update accordingly for affected occurrences And an audit entry records the timezone change and the count of affected future occurrences
Per-occurrence exception retains series rule for others
Given a weekly series pinned = host at 11:00 host local (America/Los_Angeles) And the occurrence on 2025-10-20 is edited as a single-instance exception to 11:30 host local When rendering the series across the subsequent DST transition Then the edited 2025-10-20 occurrence remains at 11:30 host local with UTC recomputed for that date And all other occurrences remain at 11:00 host local per the series rule And clearing the exception reverts that occurrence to 11:00 host local
DST impact indicator appears on affected upcoming occurrence(s)
Given a series where the next DST shift will change the computed UTC time while keeping local time stable per the pin rule When viewing the series detail or upcoming occurrences list Then the first occurrence whose UTC will differ from the previous occurrence solely due to the DST shift shows a DST indicator containing: shift date, impacted participant(s), and UTC delta (+/- hours) And the same indicator is visible to both host and client And the indicator is not shown if no UTC change will occur for the next occurrence
Buffers, availability windows, and no-overlap honored after DST recompute
Given host availability window = 09:00–17:00 host local and post-session buffer = 15 minutes And an existing appointment ends at 17:15 including buffer And a client-pinned series occurrence recomputes after DST to start at 17:10 host local When the calendar renders and validates constraints Then that occurrence is flagged "Buffer overlap" and "Outside availability" for the host And the system does not auto-shift the time and surfaces the conflict in UI and via API And no-overlap rules prevent new bookings that would overlap with either event's buffered time
Proactive DST Heads-Up Notices
"As a therapist, I want advance heads-up notifications about upcoming DST impacts so that I can review and confirm any changes before clients are affected."
Description

Send configurable advance notifications when an upcoming DST change will affect any future sessions. Provide clear before/after time previews for both host and client, with localization and time-zone-specific phrasing. Deliver via in-app banner, email, and optional SMS/push, with batched digests to reduce notification load. Allow per-workspace lead-time settings (e.g., 14 and 3 days prior) and per-series overrides. Include deep links to review impact, simulate outcomes (keep host time vs keep client time), and initiate one-tap rebase. Respect user notification preferences and compliance requirements.

Acceptance Criteria
Lead-Time DST Detection and Notification Scheduling
Given workspace lead-time settings of 14 and 3 days and optional per-series overrides When a future DST change will alter the local time of any upcoming occurrence by ≠0 minutes for either participant Then the system schedules one notification event per recipient per affected series at each applicable lead time And does not create duplicate events for the same series/lead-time after reschedules or edits within that window And excludes series with no remaining occurrences in the next 180 days And records the scheduled send times in UTC with the recipient’s current timezone snapshot
Multi-Channel Delivery and Daily Digest Batching
Given a recipient has enabled in-app and email and optional SMS/push for DST notices When multiple series are affected on the same day at a given lead time Then the system delivers a single digest per channel per recipient for that day And the digest lists each affected series with next occurrence date, before/after times, and a Review Impact deep link And SMS digest is limited to 3 series with a link to view all in-app/email And push payload includes a total affected count and navigates to the digest view And no message is sent on channels the recipient has disabled
Localized Before/After Time Previews for Both Parties
Given an affected series with host and client in different time zones When composing DST heads-up notices Then the notice shows for each recipient the before/after local time and time zone (IANA name, abbreviation, and UTC offset) for the next affected occurrence And formats date/time per the recipient’s locale (12/24‑hour, day/month order, language) And includes DST phrasing appropriate to the zone (e.g., “switches from PDT to PST” or “clocks move forward 1 hour”) And falls back to English (US) if localization for the recipient’s language is unavailable
Impact Review, Simulation, and One-Tap Rebase
Given a recipient opens the Review Impact deep link from a DST notice When the impact view loads Then it is pre-filtered to the referenced series and lead-time context And displays all future impacted occurrences with before/after times for both parties And provides a simulation toggle: Keep host time vs Keep client time, updating previews instantly And offers One‑Tap Rebase that applies the selected strategy to all future occurrences, updates the series, notifies invitees, and confirms success within 5 seconds And logs the action with actor, strategy, affected count, and timestamp
Notification Preferences and Compliance Enforcement
Given per-workspace and per-user notification preferences and regional compliance requirements When preparing to send DST heads-up notices Then the system only uses channels the recipient has opted into And requires verified SMS consent before sending SMS and includes “Reply STOP to opt out” where required And includes an unsubscribe/manage-preferences link in emails And suppresses messages to recipients flagged as do-not-contact And writes an immutable delivery log with channel, timestamp, message ID, and compliance flags
Per-Series Overrides for Lead Time and Channels
Given a recurring series with an override for DST notice lead times and/or channels When scheduling notices for that series Then the override values take precedence over workspace defaults And setting the override to Inherit reverts to workspace defaults And changes to overrides update future unsent schedules within 15 minutes And all override changes are recorded in the audit trail with actor, fields changed, old/new values, and timestamp
In-App Heads-Up Banner on Dashboard
Given a user has at least one affected series within a configured lead time When they view the dashboard Then an in-app banner appears summarizing the count of affected series and the next DST date, with a Review Impact button And dismissing the banner hides it until the next lead time or until all affected series are rebased or the DST date has passed And the banner state syncs across devices within 1 minute
One-Tap Rebase & Bulk Apply
"As a consultant, I want a one-tap way to rebase all affected sessions in a series so that I can fix schedules quickly without editing each event."
Description

Provide a guided action to rebase all affected occurrences in a series (or selected range) to maintain the chosen local-time intent after a DST shift. Show a diff preview of all changes, allow partial selection, and apply updates atomically with undo support. Cascade updates to reminders, conferencing links, room resources, and downstream automations (e.g., session-to-invoice mappings) without breaking references. Preserve event UIDs and maintain an audit trail of who approved and when. Enforce permissions and notify affected attendees with updated details and one-click confirmation.

Acceptance Criteria
One‑Tap Rebase: Entire Series
Given a recurring series pinned to the host’s intended local time (e.g., every Tuesday at 03:00 PM America/Los_Angeles) with at least one future occurrence after an upcoming DST shift And the user has Manage Schedule permission for the series And the rebase anchor (host or attendee intent) is selectable and defaults to host When the user initiates One‑Tap Rebase and chooses Entire Series Then the system recalculates UTC start/end for all future occurrences so each remains at the selected anchor’s intended local time post‑DST And the number of occurrences to be updated equals the number of future occurrences shown And no occurrence duration deviates from the original meeting length And the operation is prepared without applying changes yet
Diff Preview and Partial Selection (Including Date Range)
Given One‑Tap Rebase has identified M affected future occurrences When the diff preview opens Then each affected occurrence displays before/after local date/time and time zone for both the selected anchor (host or attendee) and the counterpart attendee And duration and resource details are shown for before/after And the preview provides filters for date range and attendee And the user can select/deselect individual occurrences or contiguous ranges, including Select All And the Apply button remains disabled until at least one occurrence is selected And the count of selected occurrences matches the items to be updated
Atomic Apply and Undo
Given at least one affected occurrence is selected in the diff preview When the user clicks Apply Then all selected updates are committed in a single atomic transaction And if any update fails, all changes are rolled back and a single error message summarizes the cause with a correlation ID And upon success, an Undo option is available for 10 minutes or until any occurrence in the series is edited again, whichever comes first And invoking Undo restores all prior start/end times, reminders, resources, conferencing links, and automation schedules
Cascade Updates Without Breaking References
Given selected occurrences are updated by rebase Then conferencing links remain unchanged and continue to be valid And room/resource reservations are moved to the new times or rebooked; any rebooking failures are itemized per occurrence prior to apply And reminder schedules (relative offsets) are recalculated to the new occurrence times And downstream automations (including session‑to‑invoice mappings) preserve their references and still execute on the updated schedule And no orphaned or duplicate records are created in linked systems post‑apply
Preserve Event UIDs and Audit Trail
Given a rebase apply succeeds Then each updated occurrence retains its original event UID And an audit log entry is created capturing actor identity, timestamp, scope (count and IDs of occurrences), before/after times, chosen anchor (host or attendee), reason (DST rebase), and notification results And the audit log is immutable and viewable in the series activity history by administrators
Permissions and Authorization Enforcement
Given a user without Manage Schedule permission for the series When they attempt to initiate One‑Tap Rebase Then access is denied with an actionable message explaining required permissions Given a user with Manage Schedule permission When they initiate One‑Tap Rebase Then access is granted and rebase actions are enabled And actions that modify resources (e.g., room rebooking) enforce resource‑level permissions
Attendee Notifications and One‑Click Confirmation
Given a rebase is applied with N affected occurrences and K unique attendees Then each affected attendee receives email and in‑app notifications within 2 minutes containing updated local date/time and time zone for each occurrence and a one‑click Confirm Changes action And attendee confirmations are recorded per occurrence and visible to the organizer And if an attendee declines or fails to confirm within 48 hours, the organizer is alerted to review
Calendar Sync & ICS Integrity
"As a freelancer, I want calendar sync updates to propagate correctly to Google/Outlook so that clients see the right time on their calendars after DST changes."
Description

Ensure accurate propagation of DST-induced updates to external calendars (Google, Outlook, Apple). Maintain stable UIDs and correct SEQUENCE increments on ICS updates, honoring RRULE behavior across DST boundaries. Reconcile organizer/attendee time representations to avoid duplicate or ghost events. Handle API/webhook edge cases, retries, and rate limits; validate rendering consistency across providers. Keep conferencing details in sync and adjust reminder schedules to match the new occurrence times. Provide health metrics and reconciliation tools to diagnose sync discrepancies.

Acceptance Criteria
UID Stability and SEQUENCE Increment on DST Shift
Given a recurring series (UID=u1, RRULE:FREQ=WEEKLY) scheduled at a fixed local time and an upcoming DST transition When SoloPilot issues the DST rebase update to external calendars Then the ICS/iTIP update retains UID=u1, increments SEQUENCE by +1 from the last known value, preserves ORGANIZER/ATTENDEE and PARTSTAT values, updates DTSTAMP, keeps SUMMARY/LOCATION/DESCRIPTION/conferencing fields unchanged, represents DTSTART/DTEND with TZID preserving the wall-clock time, and results in zero duplicates across Google, Outlook, and Apple (existing event is updated in place)
RRULE Recurrence Integrity Across DST Boundary
Given a recurring event defined by RRULE at a fixed local start time spanning a DST boundary When the series is viewed in Google, Outlook, and Apple after SoloPilot’s DST rebase Then each occurrence within ±4 weeks of the DST change starts at the intended local time, the RRULE remains unchanged, no extra or missing occurrences are present (count matches expectation), overridden instances (RECURRENCE-ID) preserve their specific local times, and EXDATEs continue to suppress the intended instances
Organizer–Attendee Time Reconciliation and Duplicate Prevention
Given a weekly series where the organizer and attendee are in different time zones with differing DST rules When SoloPilot applies a DST rebase update Then both parties see exactly one event per occurrence (no ghost or duplicate events), the event starts at each person’s intended local time, prior ATTENDEE PARTSTAT values persist, and provider APIs show orphan/duplicate count = 0 for the UID across affected occurrences
External Calendar Update Propagation with Rate-Limit Resilience
Given N affected series and provider APIs intermittently returning 429/5xx When SoloPilot dispatches DST rebase updates Then requests employ exponential backoff with jitter (initial delay ≤ 1s, factor ≥ 2, max delay ≤ 60s), retries are capped at 6 attempts per event per provider with idempotency to prevent duplicate updates, ≥99% of updates succeed within 10 minutes end-to-end, remaining items are surfaced as retriable with a next automatic attempt within 1 hour, and permanently failed items are dead-lettered with actionable error codes visible in reconciliation tools
Conferencing Details Consistency After Time Rebase
Given an event/series with conferencing details (join URL and dial-in) When a DST rebase update is issued Then conferencing details remain unchanged when only the time shifts; if the conferencing provider requires regenerated details, the new values replace the old consistently for organizer and attendees; after propagation there are no mixed states where organizer/attendee or master/overrides disagree on conferencing details across Google, Outlook, and Apple (mismatch count = 0)
Reminder Schedule Realignment and Provider Rendering Validation
Given a series with default reminders configured (e.g., 30 minutes and 1 day before start) When SoloPilot applies a DST rebase Then reminders fire relative to the updated start time with the same offsets for Google, Outlook, and Apple, no duplicate reminders are generated for any occurrence, and provider UIs render the updated start at the intended local time for both organizer and attendees
Health Metrics and Reconciliation Tooling for Sync Discrepancies
Given health monitoring is enabled When any provider occurrence time deviates by >1 minute from SoloPilot’s intended time or a duplicate/ghost is detected Then a discrepancy record is created capturing UID, provider, RECURRENCE-ID, last SEQUENCE, and error class; users can trigger Reconcile to re-emit correct ICS/iTIP, after which provider times match intended times and the discrepancy auto-closes; dashboards and APIs expose counts by provider, mean time to heal, and failure rates for the last 7 and 30 days
Conflict Detection & Smart Suggestions
"As a practitioner, I want the system to flag conflicts and suggest alternative times when DST shifts move sessions outside my working hours so that I can resolve issues efficiently."
Description

After DST recalculation or rebase, automatically detect conflicts with existing bookings, buffers, location travel time, or working-hour constraints for all participants. Surface impact summaries (e.g., overlapped sessions, outside-hours occurrences) and propose optimized alternatives ranked by least disruption. Support bulk resolution with per-client preferences and soft holds on proposed slots. Notify clients of proposed changes with approval links and auto-confirm if SLAs or policies permit. Log decisions and update related automations accordingly.

Acceptance Criteria
Post-DST Recalculation Conflict Detection
Given a series of up to 100 upcoming sessions with intended local times pinned for host and clients When a DST shift triggers recalculation or a user taps Rebase for the series Then each occurrence is recalculated to the participant’s intended local time and evaluated for conflicts against existing bookings, working-hour constraints, travel time, and configured buffers for all participants And an impact summary is generated showing counts per conflict type: overlaps, outside-hours, buffer violations, travel-time violations And each impacted occurrence includes a machine-readable reason code and human-readable explanation And the detection completes within 10 seconds for 100 occurrences or fewer And if no conflicts are found, the UI explicitly displays All clear and no suggestions are generated
Optimized Alternative Suggestions Ranked by Least Disruption
Given one or more conflicted occurrences identified by DST recalculation When the system generates alternatives Then it produces 1–3 conflict-free slots per affected occurrence that honor all participants’ working hours, buffers, and travel-time constraints And it respects per-series intended local time by prioritizing same-local-time options before shifting by ±15, ±30, or ±60 minutes And each alternative is assigned a deterministic disruption score that penalizes deviation from intended local time, outside-hours use, added travel/buffer violations, and additional participants impacted And alternatives are ranked ascending by disruption score with ties broken by earliest start time And if no viable alternative exists, the system returns No viable alternative and surfaces the blocking constraints involved
Bulk Resolution With Per-Client Preferences and Soft Holds
Given multiple conflicted occurrences across different clients When the host selects Bulk resolve and chooses a resolution rule (e.g., prefer same local time, minimize shifts, skip Fridays) Then the system applies per-client preferences (earliest/latest start, no-morning/evening windows, blackout dates, remote-only, day-of-week) and excludes any slots violating them And it places soft holds on selected proposed slots on both host and client calendars for a default 24-hour hold window (configurable 1–72 hours) And soft holds prevent double-booking, are visibly labeled as Held (Proposed), and auto-release on decline or expiry And bulk actions may be applied to all, a filtered subset, or per-client groups, with a summary of successes, failures, and reasons And partial resolutions preserve remaining suggestions and holds are not created for clients lacking any viable option
Client Notifications With Approval Links and Auto-Confirm Policies
Given proposed changes exist for one or more clients When notifications are sent Then each client receives an in-app and email notification within 1 minute containing affected session details, proposed slot(s), and a hold expiry timestamp And approval links enable Accept, Decline, or Suggest different time without login when policy allows, with secure signed tokens valid until hold expiry And upon client Accept, the session confirms and calendar invitations are updated; upon Decline, the hold is released and the host is prompted with next-best alternatives And if account policy permits auto-confirm (e.g., confirm if no response within 24 hours), the session auto-confirms at expiry and the client is notified of the confirmation And all notifications are retried on transient failures and logged with delivery status
Impact Summaries and Conflict Reason Transparency
Given conflicts are detected for a series after DST recalculation When the user opens the Impact Summary view Then the UI displays aggregate counts by conflict type and total affected clients/sessions And each affected session row shows the reason codes (e.g., OVERLAP, OUTSIDE_HOURS, BUFFER_VIOLATION, TRAVEL_VIOLATION) with tooltips describing the decision logic And the user can filter the list by conflict type and by client, and export a CSV including session IDs, original time, proposed time(s), and reason codes And the summary updates in real time as resolutions are applied or holds expire
Travel-Time and Buffer-Aware Detection Across Locations
Given consecutive sessions at different physical locations with configured buffer times When DST recalculation shifts one or more sessions Then the system computes travel windows using the configured travel-time provider (or a default static estimate) and enforces pre/post buffers And a conflict is raised if the recalculated time plus travel and buffer overlaps another session or violates working hours And suggested alternatives only include slots that keep travel+buffer feasible between adjacent sessions based on the same travel-time assumptions And the reason and required minimum gap are shown for any travel-related conflict
Audit Logging and Automation Updates
Given any decision is made on a proposed DST-related change (manual accept/decline, client response, auto-confirm, expiry) When the decision is finalized Then an immutable audit log entry is recorded with timestamps, actor (host/client/system), policy applied, original and new times, reason codes, notification delivery outcomes, and hold identifiers And related automations are updated: reminders rescheduled, external calendar events updated or reissued, invoice drafts tied to session date/time adjusted, and webhooks emitted exactly once with idempotency keys And the audit trail is queryable by session ID and exportable for compliance And failures to update automations are surfaced with actionable retry options and do not silently drop changes
Admin Controls & Defaults
"As a workspace owner, I want controls to set default DST policies and notification rules so that my team’s scheduling behavior is consistent and compliant."
Description

Add workspace-level settings for DST policy defaults: pinning strategy, notification lead times, channels, auto-rebase permissions, and client-facing messaging templates. Allow per-client and per-series overrides, with role-based access control for who can approve rebases and send notices. Expose audit logs and exports for compliance. Provide API endpoints and import/export for configuration-as-code. Ensure sensible defaults for solo users while supporting advanced requirements for small teams sharing a SoloPilot workspace.

Acceptance Criteria
Workspace DST Default Settings Management
- Given I am a Workspace Admin and DST Guardian is enabled When I open Workspace Settings > DST Guardian and save defaults including: pinning_strategy in {host_local_time, attendee_local_time, fixed_utc_offset} notice_lead_times ⊆ {14d,7d,24h} and not empty channels ⊆ {email,SMS,in_app} auto_rebase in {auto,require_approval} templates present for {heads_up,rebase_request,rebase_confirmation} Then the system validates inputs, persists settings, returns 200, and applies these defaults to new clients/series created after save - Given invalid values are submitted (e.g., empty lead_times or unknown channel) When I attempt to save Then the save is rejected with 422 and field-level errors identifying each invalid field - Given I click "Reset to System Defaults" When I confirm Then settings revert to SoloPilot system defaults and an audit event is recorded
Per-Client/Series Overrides and Inheritance
- Given workspace defaults exist and a client-level override is set for pinning_strategy=attendee_local_time When I create a new recurring series for that client Then the series inherits pinning_strategy=attendee_local_time - Given a series-level override is set Then the effective DST policy for that series equals the series override regardless of client/workspace values - Given I clear a client or series override When I save Then the effective policy immediately reverts to the next level (series→client→workspace as applicable) and is reflected in UI and API within 5 seconds - Given conflicting values exist at different levels Then precedence is series > client > workspace and the effective policy panel displays the resolved value and its source
RBAC for Rebase Approvals and Notices
- Given role permissions are configured and a user lacks approve_rebase or send_dstonotice When the user attempts to approve a rebase or send DST notices Then the action is hidden/disabled in UI and API calls return 403 with an authorization error code - Given a user has the required permission When they approve or decline a rebase Then the action succeeds, affected session IDs are listed in the response, and an audit entry captures user_id, role, action, session_ids, and outcome - Given a multi-seat workspace using defaults Then Owners and Admins are approvers by default and only they receive approval tasks/notifications until permissions are changed
Auto‑Rebase Policy and One‑Tap Approval Flow
- Given auto_rebase=auto and a DST shift is detected for a series When the shift enters the earliest configured lead_time window Then affected session times rebase automatically per pinning_strategy and notices are sent to all parties using the configured templates - Given auto_rebase=require_approval and a DST shift is detected Then a rebase approval task is created, approvers receive a one‑tap Approve/Decline link (single‑use, valid 48h), and no session times change until approval - Given an approver taps Approve from email/SMS/web Then all affected sessions rebase atomically, the approver sees a success confirmation, recipients receive rebase_confirmation, and duplicate taps are idempotently ignored - Given an approver declines Then sessions remain unchanged, recipients receive rebase_declined messaging, and a decline_reason is stored and visible in audit log
Notification Lead Times and Channels Execution
- Given lead_times are [14d,7d,24h] and channels include {email,SMS} When a DST shift is 15 days away Then the 14d notice is queued and delivered at 14 days before the shift in each recipient’s local time via email and SMS - Given multiple sessions in the same series are affected Then a single consolidated notice per lead_time per series is sent to each recipient listing all affected sessions and their before/after local times - Given a channel is deselected at workspace or overridden at client/series Then no notices are delivered via that channel for that scope - Then each notice includes: sender/recipient names, current local time, post‑rebase local time, date, timezone abbreviations, and a clear call‑to‑action where applicable - Then delivery status (queued, sent, failed, opened where available) is captured per recipient and channel
Audit Logging and Compliance Export
- Given any of: settings change, override change, approval/decline, auto‑rebase applied, notice sent When the event occurs Then an immutable audit record is stored with: timestamp_utc, actor (user_id/system), actor_ip, event_type, object_type/id, before/after, request_id, and hash - Given I filter audit logs by date range, actor, client, series, or event_type When I export via UI or GET /api/v1/audit/export?format=csv Then I receive a file containing only matching records with CSV header, totals, and checksum in the footer; HTTP 200 - Given my role lacks view_audit_export When I attempt export Then access is denied with HTTP 403 and no file is generated
Configuration‑as‑Code API: Import/Export and Versioning
- Given a token with scope=config:read When I GET /api/v1/dst/settings Then the response is 200 with settings JSON including schema_version, etag, updated_at, and effective values - Given a token with scope=config:write When I PUT /api/v1/dst/settings with If‑Match etag and a valid payload Then the response is 200, the version increments, a new etag is returned, and an audit event is recorded - Given dry_run=true is provided When I PUT a payload Then the response is 200 with plan {changes[], warnings[]} and no changes are applied - Given I POST /api/v1/dst/import with a signed YAML/JSON export Then the system validates signature and schema, applies idempotently using upsert semantics, and returns 409 on version conflict without If‑Match - Given I GET /api/v1/dst/export?format=yaml Then the response is 200 with a canonical export that round‑trips on re‑import without spurious diffs

TravelSense Profiles

Detects travel and temporary timezone changes from calendar locations, email signatures, or user prompts. Lets clients set trip windows and auto‑adjusts booking displays and reminder timing during travel. Keeps schedules accurate on the move without manual recalcs.

Requirements

Multi-Source Travel Detection Engine
"As a traveling consultant, I want SoloPilot to automatically detect when I’m away based on my calendar locations or email signature so that my schedule and reminders adjust without manual recalculations."
Description

Implements an automated detector that infers travel and temporary timezone changes by analyzing calendar event locations, optional email signature cues, and explicit user prompts. Normalizes detected places via geocoding to canonical timezones, infers trip windows from first/last relevant events, and scores detections to minimize false positives. Consolidates overlapping signals, resolves conflicts, and creates a pending TravelSense profile for user confirmation. Runs as a background job with rate limiting and retries, stores minimal metadata, and exposes detection results to the Trip Window Management UI. Designed to be source-pluggable for future inputs and resilient to missing or ambiguous data.

Acceptance Criteria
Calendar Location-Based Detection and Trip Window Inference
Given the user’s home timezone is configured and the calendar contains >= 2 events with geocodable locations mapping to the same non-home timezone within a 5-day span When the detection engine runs Then it geocodes the event locations to a canonical place and timezone, infers a trip window from the first to last qualifying event (inclusive), computes a confidence >= 0.70, and creates a pending TravelSense profile with that window and timezone And events with non-geocodable or virtual keywords ["Zoom","Remote","Virtual","Online","TBD"] are excluded from qualification (case-insensitive) And no profile is created if the normalized timezone equals the user’s home timezone
Email Signature Timezone Cue Detection
Given the email ingestion service provides parsed signature cues for the user’s recent emails within the last 7 days and rate limits are not exceeded And either (a) >= 2 distinct emails contain city/airport/offset cues that geocode to the same non-home timezone or (b) a single email contains both a city and explicit UTC offset (e.g., GMT+9) that concordantly geocode When the detection engine runs Then it normalizes the place to a canonical timezone, computes confidence >= 0.60, and creates a pending TravelSense profile with the inferred window [first email date, last email date] And only minimal metadata is stored (city/place_id, timezone, dates, source type, confidence, hashed cue snippet); raw email bodies and full signatures are not persisted
Explicit User Prompt Creates High-Confidence Travel Profile
Given the user submits a prompt indicating travel (e.g., "I’m in London next Mon–Thu") via the supported input channel When the detector parses the prompt Then it geocodes the place to a canonical timezone, extracts the explicit date window, sets confidence = 1.00, and creates a pending TravelSense profile with that window and timezone And if dates are partial (e.g., start only), the window defaults to start + 2 days and confidence is capped at 0.85
Multi-Source Signal Consolidation and Conflict Resolution
Given >= 2 detections/signals for the user overlap in time by at least 1 day and their places geocode within 80 km or share the same canonical timezone When the consolidator runs Then a single pending TravelSense profile is produced with a window from min(start) to max(end) across merged signals, sources aggregated, and a final timezone chosen by precedence (user_prompt > calendar > email) else by highest confidence And if candidate windows do not overlap by >= 1 day, separate pending profiles are retained And at most one pending profile exists per overlapping cluster after consolidation
False Positive Suppression via Scoring and Rules
Given signals that (a) are singletons below source thresholds, or (b) contain virtual/non-physical locations ["Zoom","Remote","Virtual","Online","TBD"], or (c) normalize to the user’s home timezone When the detection engine scores the signals Then no pending TravelSense profile is created and the computed confidence is < the configured acceptance threshold (default 0.70) And such rejections are logged with reason codes without persisting raw source content
Background Job Scheduling, Rate Limiting, Retries, and Idempotency
Given the background detector is scheduled to run every 15 minutes per user with a configured per-source rate limit When transient errors (e.g., HTTP 429/5xx) occur during source fetches Then the job retries with exponential backoff (1s, 2s, 4s, 8s, max 3 retries), respects per-source rate limits, and ensures idempotency via a dedup key (user_id + source + window hash) so duplicate profiles are not created And 95th percentile job time per user is <= 2s for 100 signals and failures are recorded with retry metadata
Expose Pending Detections to Trip Window Management UI
Given pending TravelSense profiles exist for a user When the UI requests GET /travelsense/detections?status=pending Then the API responds within 200 ms p95 with a list sorted by confidence desc, each item containing [id, sources, place_name, canonical_timezone, start_at, end_at, confidence, created_at] And the payload excludes raw email text and full calendar descriptions and includes available actions [confirm, dismiss] And upon confirm, the profile status becomes active and the underlying signals are archived
Trip Window Management UI
"As a solo practitioner, I want to confirm or edit detected trip windows so that I stay in control of when and how my schedule changes."
Description

Provides a dedicated interface to review, confirm, edit, or dismiss detected trips and to create trips manually. Allows setting trip start/end dates, primary city/region, target timezone, working hours during travel, and which surfaces to adjust (booking pages, internal scheduler, reminders). Displays detection confidence, source(s), and any conflicts with existing events. Supports overlapping trips resolution, per-trip preferences (dual-time display, quiet hours), and an activity log of changes. Accessible from settings and surfaced contextually when a new detection occurs.

Acceptance Criteria
Confirming a Detected Trip from Contextual Prompt
Given a new trip is detected from a calendar location or email signature with confidence >= 50% When the user opens SoloPilot after the detection Then a contextual prompt is displayed within 2 seconds showing start date, end date, primary city/region, derived timezone, working hours (default), detection confidence (0–100%), and source(s) And any scheduling conflicts with existing events during the proposed window are listed with counts and links And Confirm, Edit, and Dismiss actions are available When the user selects Confirm Then the trip is saved as Active, appears in Settings > TravelSense > Trips, and the prompt is dismissed And booking pages and the internal scheduler display the trip timezone for dates within the window within 60 seconds And reminder timing for meetings inside the window is recalculated to the trip timezone within 60 seconds And an activity log entry is created with action Confirmed Detected Trip, timestamp, actor, sources, and confidence
Manually Creating a Trip With Validation and Defaults
Given the user is on Settings > TravelSense > Trip Window Management When the user clicks Create Trip Then a form is shown with fields: Start Date (required), End Date (required), Primary City/Region (required), Timezone (required, defaults to region), Working Hours During Travel (optional, defaults to account business hours), Surfaces to Adjust [Booking Pages, Internal Scheduler, Reminders] (all enabled by default), Dual-Time Display (toggle, default off), Quiet Hours (optional time range) And the form validates: End Date >= Start Date; Timezone is a valid IANA identifier; time ranges are within 00:00–23:59 with Start < End When required fields are empty or invalid Then inline errors are shown next to the fields and Save is disabled When all validations pass and the user clicks Save Then the trip is created and listed as Active with the configured preferences And if the trip overlaps an existing trip, an overlap resolution dialog is triggered instead of saving directly
Resolving Overlapping Trips Before Save
Given a new or edited trip's date range overlaps one or more existing trips When the user clicks Save Then an overlap resolution dialog lists each conflicting trip with date ranges and surfaces affected And the user must choose one: Merge trips; Keep both and set priority; Adjust dates If Merge trips is chosen Then fields are merged by latest edit timestamp, surfaces enabled = union, and timezone = higher-confidence source; user can review before confirming If Keep both is chosen Then the user sets a priority order; the higher-priority trip governs timezone, working hours, and surfaces on overlapping dates; deterministic tie-breaker = most recently updated If Adjust dates is chosen Then the UI permits editing the new trip’s start/end until no overlap remains And Save remains disabled until a resolution option is confirmed And upon confirmation, the trip(s) save, and an activity log entry records the resolution method and details
Editing an Existing Trip and Writing to Activity Log
Given an existing trip is Active When the user edits any field and clicks Save Then changes are persisted and reflected in the trip detail view within 2 seconds And an activity log entry records each changed field with old value -> new value, timestamp, and actor And booking pages, internal scheduler, and reminders apply updated timezone/working hours within 60 seconds for all future events inside the trip window And if date changes cause new overlaps, the overlap resolution dialog is invoked before saving
Dismissing a Low-Confidence Detected Trip
Given a detected trip with confidence < 50% is presented in the contextual prompt or the Trip Review queue When the user selects Dismiss Then no trip is created and the detection is archived with status Dismissed And subsequent detections matching the same dates and city/region from the same source are suppressed for 30 days unless new corroborating sources are found And the user can Undo within 30 seconds to restore the detection to Review And an activity log entry records Dismissed Detected Trip with source(s), confidence, timestamp, and actor
Displaying Detection Confidence, Sources, and Event Conflicts
Given a detected trip is opened in the review UI Then the UI displays a numeric confidence score (0–100%) rounded to the nearest integer and a qualitative label [Low, Medium, High] using thresholds [0–49, 50–79, 80–100] And sources are listed with type (Calendar, Email, Prompt), timestamps, and links to the originating item where available And conflicts with existing scheduled events are summarized: count of events outside working hours and count of overlapping trips, each with a view link that filters the calendar to the trip window And the UI renders a non-blocking warning when conflicts exist
Applying Per-Trip Preferences: Surfaces, Dual-Time, and Quiet Hours
Given an Active trip with per-trip preferences configured When Surfaces to Adjust include Booking Pages and Internal Scheduler Then all booking pages show the trip timezone for dates inside the window and revert outside it; the internal scheduler displays both home and trip time if Dual-Time is enabled When Dual-Time Display is enabled Then event cards and availability slots show both timezones with clear labels and offsets (e.g., 10:00 PT / 19:00 CET) When Quiet Hours are set (e.g., 21:00–07:00 trip local time) Then reminder sends scheduled inside quiet hours are deferred to the next allowed minute after quiet hours end, preserving configured relative offsets And scheduling suggestions do not propose times inside quiet hours And disabling any surface removes its adjustments within 60 seconds
Auto-Timezone Adjustment for Booking Surfaces
"As a coach, I want clients to see my availability in the correct local time when I’m traveling so that they book accurate slots and I avoid missed or late sessions."
Description

Automatically adjusts all booking-related displays during active trip windows. Public booking pages and the internal scheduler render availability in the traveler’s active timezone while preserving event storage in UTC. Offers dual-time display (home vs travel) and a travel banner to reduce confusion. Honors trip-specific working hours, prevents accidental bookings outside configured travel hours, and reverts cleanly at trip end. Handles daylight saving transitions, recurring events, and partial-day travel with idempotent updates and clear visual indicators.

Acceptance Criteria
Public Booking Pages Render in Active Travel Timezone with Dual-Time and Travel Banner
Given a user with home timezone TZ1 and an active trip window to timezone TZ2 with dual-time enabled and trip-specific working hours configured When a visitor loads the user’s public booking page during the active trip window Then available time slots are displayed in TZ2 as the primary time and TZ1 as the secondary time for each slot And a persistent travel banner is visible at the top indicating TZ2 (abbreviation) and the trip date range And no slots are shown outside the configured trip-specific working hours in TZ2 And the booking page API responses include UTC timestamps for slots with an accompanying tz field set to TZ2
Internal Scheduler Renders in Travel Timezone While Preserving UTC Storage
Given a user has an active trip window with timezone TZ2 and home timezone TZ1 When the internal scheduler is opened during the active trip window Then all calendar grids, slot labels, and time pickers render in TZ2 And creating or editing a session shows times in TZ2 in the UI but persists the event start/end in UTC without altering the UTC values on subsequent reads And exported ICS/Cal events include UTC timestamps with correct TZ2 display metadata
Enforce Trip-Specific Working Hours Across Booking Surfaces
Given trip-specific working hours are defined as 10:00–16:00 TZ2 Monday–Friday for an active trip window When availability is computed for the public booking page and internal scheduler during the trip Then only slots within 10:00–16:00 TZ2 are generated And attempts to create or book outside these hours are blocked with a clear validation message And disabled times are visually indicated with a tooltip explaining trip-hour restrictions
Automatic Reversion to Home Timezone at Trip End
Given a trip window ends at a specific timestamp When the current time passes the trip end timestamp Then the public booking page and internal scheduler revert to rendering in the home timezone TZ1 within 5 minutes And the travel banner and dual-time indicators are removed or reset per user settings And no trip-specific working hours continue to restrict availability after the trip ends And events created during travel continue to display correctly in TZ1 while their stored UTC values remain unchanged
Handle Daylight Saving Transitions and Partial-Day Travel
Given the active travel timezone TZ2 experiences a DST transition during the trip When availability spans the DST change Then slot times reflect the correct local offsets with no duplicate or missing slots beyond what DST mandates And recurring and one-off events retain their intended local times across the transition Given a trip starts or ends mid-day (e.g., start at 14:30 local) When computing availability on the boundary days Then timezone adjustment applies only from the start timestamp to the end timestamp inclusive; times before/after use the appropriate non-travel timezone
Recurring Events Across Travel and Home Periods Display Correct Local Times
Given a recurring series spans dates before, during, and after an active trip window When viewing the series on the internal scheduler and public booking surfaces Then each occurrence displays in the correct local timezone for its date (TZ1 outside the trip, TZ2 during the trip) And editing a single occurrence during the trip updates only that occurrence without shifting past or future occurrences And editing the entire series respects UTC anchors and applies correct local offsets without retroactive drift
Idempotent Updates and Consistent Visual Indicators
Given multiple inputs update the active trip window (e.g., calendar location detection and manual profile change) When the system recalculates availability and UI indicators Then updates are idempotent with no duplicate banners, no double-shifted times, and exactly one availability recomputation per change set And the travel banner text and dual-time indicators are identical across the public booking page and internal scheduler And a single audit event is recorded per change (e.g., timezone_adjustment_applied) with timestamp and source
Travel-Aware Reminder and Notification Scheduling
"As a therapist, I want reminders to adjust to the right local time when I travel so that clients and I receive prompts at expected hours."
Description

Shifts email/SMS reminders, confirmations, and follow-ups to align with the active travel timezone or the recipient’s timezone as configured. Supports configurable lead times, per-trip quiet hours, and offsets for different event types. Ensures consistent send windows across DST changes, logs adjustments for auditability, and degrades gracefully to home timezone if detection is absent. Integrates with existing messaging pipelines without altering message content, only timing, and exposes per-trip preview to validate schedules before activation.

Acceptance Criteria
Timezone Selection and Fallback Hierarchy
Given the user’s home timezone is America/Los_Angeles and an active trip timezone America/New_York is set from 2025-10-01 to 2025-10-10 And an appointment is scheduled for 2025-10-06 10:00 America/New_York And reminder lead time is 24 hours When scheduling reminders with "Send in recipient timezone" enabled for Client A whose timezone is Europe/London Then the reminder is scheduled for 2025-10-05 15:00 Europe/London Given the same appointment and lead time And "Send in recipient timezone" is disabled for Client B When scheduling the reminder during the active trip window Then the reminder is scheduled for 2025-10-05 10:00 America/New_York Given the same appointment and lead time And there is no active trip detected and no recipient timezone configured When scheduling the reminder Then the reminder is scheduled for 2025-10-05 07:00 America/Los_Angeles
Per-Trip Quiet Hours Enforcement
Given an active trip timezone America/New_York with quiet hours 21:00–07:59 And an appointment on 2025-10-06 10:00 America/New_York And a reminder lead time of 12 hours (computed send = 2025-10-05 22:00 America/New_York) When the system schedules the reminder Then the send time shifts to 2025-10-06 08:00 America/New_York (next allowed window) Given an appointment on 2025-10-06 08:00 America/New_York And a reminder lead time of 1 hour (computed send = 2025-10-06 07:00 America/New_York, within quiet hours) When the system schedules the reminder Then the send time is 2025-10-06 07:59 America/New_York (last allowed minute before event) And the adjustment reason is recorded as QUIET_HOURS_SHIFT
Event-Type Offsets: Confirmations, Reminders, Follow-Ups
Given an active trip timezone America/New_York And booking occurs at 2025-10-01 09:12 America/New_York for an appointment on 2025-10-06 10:00–11:00 America/New_York And offsets are configured as: Confirmation = 0 minutes after booking; Reminder = 24 hours before start; Follow-up = 2 hours after end When the system schedules notifications Then the confirmation is scheduled for 2025-10-01 09:12 America/New_York And the reminder is scheduled for 2025-10-05 10:00 America/New_York And the follow-up is scheduled for 2025-10-06 13:00 America/New_York And email and SMS channels for the same notification share the same scheduled timestamp
DST Change Normalization for Send Windows
Given an active trip timezone America/New_York And a reminder lead time of 24 hours When the appointment is on 2025-11-03 10:00 America/New_York (DST ended on 2025-11-02) Then the reminder is scheduled for 2025-11-02 10:00 America/New_York (same wall-clock time minus lead) When the appointment is on 2025-03-10 10:00 America/New_York (DST began on 2025-03-09) Then the reminder is scheduled for 2025-03-09 10:00 America/New_York (same wall-clock time minus lead)
Audit Log of Scheduling and Adjustments
Given any notification is scheduled or rescheduled When the schedule is computed Then an audit record is created containing: message_id, event_id, actor="system", computed_at (ISO 8601 UTC), original_scheduled_at (UTC), final_scheduled_at (UTC), timezone_used (IANA zone), adjustment_reason (one of: TRAVEL_TIMEZONE, RECIPIENT_TIMEZONE, QUIET_HOURS_SHIFT, DST_NORMALIZATION, HOME_TIMEZONE_FALLBACK, NONE) And audit records are immutable and append-only on reschedule And audit history is queryable by event_id and message_id with chronological ordering by computed_at
Messaging Pipeline Integration Without Content Changes
Given the existing messaging pipelines for email and SMS are in place When Travel-Aware scheduling computes scheduled_at for a notification Then the system enqueues to the same pipeline using the computed scheduled_at without modifying template_id, subject, body, or merge variables And a SHA-256 hash of the rendered content before and after scheduling is identical And provider-specific metadata (from, sender IDs, SMS short code) remain unchanged And only one queued job per channel per notification exists (no duplicates)
Per-Trip Preview and Activation Gate
Given a user configures a trip window and settings (timezone basis, quiet hours, offsets) When the user opens the per-trip preview Then the preview lists all affected notifications with: message type, target timezone label, scheduled send time, and any adjustment reasons (quiet hours, DST, timezone basis) And the user must click "Activate" to apply the schedule; until activated, no production schedules are altered And after activation, the generated schedule version is referenced in audit logs with a preview_id/version tag
Calendar Provider Integration and Sync for Timezone Intelligence
"As a freelancer, I want SoloPilot to use my calendar data to infer trips so that I don’t have to manually enter travel dates."
Description

Integrates with Google Calendar and Microsoft 365 to read event locations, organizer timezones, and travel-related entries. Uses OAuth with minimal scopes, respects rate limits, and leverages webhooks/polling to stay current. Normalizes provider-specific timezone identifiers and geocodes free-text locations to TZ data. Deduplicates across calendars, skips private event content beyond required fields, and processes recurring/updated events reliably. Exposes a sync status panel and error handling with retry policies.

Acceptance Criteria
Privacy, Consent, and Data Minimization Controls
"As a privacy‑conscious user, I want to control which data sources are used and how long data is kept so that my personal information remains secure."
Description

Adds explicit opt-in controls for each detection source (calendar, email signature, manual prompts), with clear consent text and the ability to revoke at any time. Limits stored data to derived timezone, trip window, and normalized city; redacts raw email content and free-text locations after processing with configurable retention. Provides a data export and deletion path, audit logs of detections and adjustments, and admin guardrails for API scopes. Ensures encryption at rest/in transit and adheres to regional data residency settings where applicable.

Acceptance Criteria
Per-Source Consent Toggle and Revocation
Given a user opens TravelSense > Privacy Controls, When viewing detection sources, Then each source (Calendar Location Parsing, Email Signature Parsing, Manual Travel Prompts) has an independent opt-in toggle defaulted to Off. Given a user enables any source, When saving changes, Then a consent dialog displays purpose, specific data elements processed, retention policy link, and data residency notice, and activation is blocked until an explicit checkbox is checked. Given consent is granted for a source, When the user revokes that source, Then new detections from that source cease within 60 seconds and any queued jobs for that source are cancelled. Given consent is revoked for a source, When auditing the consent store, Then the system records the revocation timestamp, consent version, and actor, and the source status shows Disabled across web and API within 60 seconds.
Data Minimization: Persist Only Derived Fields
Given a detection completes, When retrieving the TravelSense profile via API, Then only derived_timezone, trip_window_start_utc, trip_window_end_utc, normalized_city_name, and normalized_city_id are returned for the trip context. Given a detection completes, When inspecting audit records via the admin UI/export, Then no raw email body, email signature text, calendar description, or free-text location strings are stored; only normalized/derived fields and metadata appear. Given a detection completes, When searching storage by message-id or raw location text, Then zero records are returned outside the raw_input retention store. Given a detection completes, When verifying indexes/snapshots, Then no backups contain raw inputs beyond the configured retention window.
Configurable Retention and Automatic Redaction of Raw Inputs
Given an admin sets raw input retention to N days (0–30), When a detection processes raw email/signature/calendar text, Then the raw content is encrypted at rest and scheduled for redaction at N days from capture (N=0 redacted within 5 minutes). Given the redaction job runs, When the retention threshold is reached, Then the system irreversibly deletes raw content and replaces it with a redaction marker containing timestamp, source, and hash-of-content for integrity (no reconstructable text). Given a user requests their raw inputs via export after redaction, When generating the export, Then no raw content is included and only derived fields and redaction markers appear. Given retention is decreased (e.g., from 7 to 0 days), When saving the new policy, Then all eligible existing raw inputs are queued for immediate redaction and complete within 15 minutes.
Self-Service Data Export and Deletion Path
Given a user requests a TravelSense privacy export, When the request is confirmed, Then a machine-readable export (JSON) containing consents, derived fields, and related audit entries is produced within 24 hours and a signed link (7-day expiry) is provided. Given a user submits a deletion request, When the request is confirmed, Then derived TravelSense fields and any raw inputs under retention are deleted/redacted within 24 hours, and personal fields in audit logs are pseudonymized (event type retained) with a completion receipt issued. Given a deletion request completes, When attempting to trigger a new detection, Then no historical data is used and the profile starts empty until new explicit consents are granted. Given a user cancels an in-progress export/deletion, When cancellation is accepted, Then processing halts and state is logged without partial data exposure.
Tamper-Evident Audit Logs for Detections and Adjustments
Given a detection or timezone adjustment occurs, When creating an audit entry, Then the record includes timestamp (UTC), source, normalized_city_id/name, derived_timezone before/after, action taken, consent version, and workspace/user identifiers, and excludes any raw input text. Given audit logs are stored, When verifying integrity, Then entries are append-only and chained with a rolling hash so that any tampering is detectable. Given an admin views audit logs, When filtering by user or time window, Then results return within 3 seconds for up to 10k records and can be exported as CSV/JSON without raw content fields. Given data residency is set, When audit entries are written, Then they are stored in-region and never replicated cross-region except for encrypted backups in the same region tier.
API Scope Guardrails and Least-Privilege Integrations
Given a workspace has not opted into Email Signature Parsing, When initiating OAuth for email providers, Then the requested scopes exclude any scope that permits reading message bodies or signatures. Given only Calendar Location Parsing is enabled, When connecting a calendar, Then requested scopes are limited to read-only event metadata (title, start/end, location) and exclude write or delete scopes. Given an admin reviews Integrations > Scopes, When viewing an integration, Then the UI shows the exact granted scopes, last-used timestamp, region, and a one-click revoke button that revokes within 60 seconds. Given a scope change is attempted by the system, When additional scopes are required, Then the user is prompted with a clear justification and must re-consent; the system cannot auto-escalate scopes.
Encryption and Regional Data Residency Enforcement
Given any TravelSense data is stored at rest, Then it is encrypted using cloud KMS-managed AES-256 (or stronger) with key rotation at least every 90 days and access limited by IAM. Given data is transmitted between client and server or across services, Then TLS 1.2+ with HSTS is enforced and weak ciphers are disabled. Given a workspace selects region X, When detections, storage, backups, and exports execute, Then all raw inputs, derived fields, and audit logs remain in region X and cross-region access is blocked by policy; attempts to set conflicting regions are rejected with a clear error. Given a region migration is initiated by an admin, When the migration runs, Then all TravelSense data is rehydrated in the target region with encryption preserved and the source region is purged after verification, with no window of dual-write beyond the cutover.
Manual Overrides and Exceptions
"As a consultant, I want a simple override to lock or adjust my timezone for specific events so that edge cases don’t disrupt my schedule."
Description

Enables users to override detection by locking timezone globally, per trip, or per event, and to set per-client exceptions (e.g., always show client’s local time). Provides a one-click kill switch to disable TravelSense adjustments, conflict resolution rules for overlapping trips, and fallback behavior when geocoding fails. Tracks overrides in the audit log and ensures overrides take precedence in all booking and reminder surfaces until cleared. Supports quick toggle from the scheduler and booking settings.

Acceptance Criteria
Global Timezone Lock Override
Given the user enables Global Timezone Lock in Settings When TravelSense receives any timezone signal (calendar location, email signature, trip detection, system time) Then all booking views, confirmations, and reminders use the locked timezone for the account holder And TravelSense suppresses automatic timezone adjustments and detection banners And the locked timezone persists across sessions and devices until manually cleared by the user And a visible "Global TZ Locked" indicator appears in Scheduler and Booking Settings And an audit log entry records lock and unlock actions with actor, timestamp, previous value, and surface
One-Click Kill Switch for TravelSense
Given the user toggles the TravelSense kill switch from the Scheduler toolbar or Booking Settings When the switch is ON Then TravelSense detection and automatic adjustments are disabled platform-wide And per-scope manual overrides (global lock, trip, event, client exception) continue to apply And booking and reminder surfaces display a "TravelSense Off" badge And the state persists until the user toggles it OFF And an audit log entry records each toggle with actor, timestamp, and surface When the switch is OFF Then normal detection resumes unless superseded by manual overrides
Per-Trip Timezone Override
Given the user creates or edits a trip with an explicit timezone and a start/end window When the current time is within the trip window Then booking displays and reminder timings for the account holder use the trip timezone And automatic detection signals are ignored for the duration of the window And outside the window, behavior reverts to Global Lock if present; otherwise to detection And the active trip override is indicated on the Scheduler during the window And create/update/delete of the trip override is recorded in the audit log
Overlapping Trip Conflict Resolution
Given two or more trips have overlapping windows with explicit timezones When the current time falls within an overlapping interval Then the active timezone is taken from the most recently updated trip (latest updatedAt) And if updatedAt timestamps are equal, the trip with the shorter duration takes precedence And the resolved timezone is applied consistently across Scheduler, booking pages, and reminders And a conflict indicator is shown with a link to manage trips And the resolution decision (trip IDs and rule applied) is written to the audit log
Per-Event Timezone Override
Given the user sets an explicit timezone on an individual event When TravelSense detection, a trip override, or a global lock would suggest a different timezone Then the event's explicit timezone is used for that event across booking surfaces, invitations, and reminders And the event detail view shows an "Event TZ Override" label with the selected timezone And clearing the event override reverts to the next applicable scope (trip > global > detection) And set/clear actions for the event override are captured in the audit log
Per-Client "Always Show Client Local Time" Exception
Given the user enables "Always show in client's local time" in a client's profile When that client views booking pages or receives reminders/confirmations Then times are presented in the client's last-known confirmed timezone; if unknown, the client is prompted to confirm timezone And the practitioner's internal calendar maintains its own timezone logic and is not altered by this exception And this exception applies regardless of TravelSense detection, trips, or global lock settings And the rule is honored in group events where the client participates And enabling/disabling the exception is recorded in the audit log
Geocoding Failure Fallback
Given TravelSense cannot resolve a location to a timezone within 2 seconds or the geocoding API returns an error When a timezone decision is required for display or reminders Then fallback to the account holder's last-known confirmed timezone; if none, use the account default timezone And show a non-blocking warning banner with options to retry or set timezone manually And do not auto-switch timezones based on ambiguous or partial signals until resolution And the failure, fallback path, and error codes are logged to telemetry and the audit log

Overlap Heatmap

Visualizes the best meeting overlaps across 2+ timezones with a color‑coded grid and fairness rotation (e.g., alternate early/late weeks). One‑tap proposes the top slots with localized times for each invitee. Cuts group scheduling from days to minutes.

Requirements

Timezone-Aware Availability Aggregation
"As an organizer, I want to import invitees’ availability across time zones so that I can see when everyone can meet without manual back-and-forth."
Description

Aggregate invitee availability across time zones by connecting external calendars (Google, Microsoft, iCal) with OAuth, respecting working hours, preferred windows, and blackout dates. Normalize events into a unified free/busy matrix for the next configurable horizon, support recurring events and real-time sync, and compute candidate blocks for a specified meeting duration. Provide caching and incremental updates to keep the overlap model current while minimizing API calls and respecting rate limits.

Acceptance Criteria
OAuth Calendar Connection Established and Validated
Given a user selects Google or Microsoft calendar for connection, When they authorize via OAuth, Then the system requests minimum read-only calendar scopes and records provider, accountId, timezone, and selected calendarIds. Given OAuth consent is granted, When the callback is received, Then access/refresh tokens are stored securely, a health-check availability fetch succeeds within 3 seconds, and the connection status is set to Connected. Given access is revoked or a 401/invalid_grant occurs on token refresh, When the next sync runs or a revocation notification is received, Then the connection status switches to Needs Reconnect within 5 minutes and no further provider calls are attempted.
Working Hours, Preferred Windows, and Blackout Enforcement
Given an invitee has defined working hours, preferred windows, and blackout dates in their local timezone, When availability is aggregated, Then time outside working hours and blackout dates is marked unavailable and never contributes to candidate blocks. Given preferred windows are defined, When the free/busy matrix is generated, Then slots inside preferred windows are flagged with preferred=true for downstream selection. Given a DST transition day, When working hours are applied, Then hours are interpreted in local wall time without shifting relative to the local clock.
Unified Free/Busy Matrix Generation for Configurable Horizon
Given a horizon is configured between 1 and 60 days (default 21) and slot granularity is 15 minutes, When normalization runs, Then a UTC-normalized free/busy matrix is produced at 15-minute resolution for each invitee covering only the upcoming horizon. Given provider events with varying time zones, When they are normalized, Then all intervals use half-open [start, end) semantics and retain the source timezone ID (IANA) for each invitee. Given the matrix is generated, When queried, Then it returns within 1 second for cached results and within 4 seconds on a cold build for up to 10 connected calendars.
Recurring Events and Exceptions Normalization
Given a recurring event with RRULE/RDATE/EXDATE within the horizon, When normalization runs, Then all instances are expanded, exceptions are applied, and canceled instances are excluded. Given event transparency/showAs is Transparent/Free, When aggregating availability, Then it does not block time; if Busy or OutOfOffice, Then it blocks; Tentative is treated as Busy by default. Given an all-day Busy/OOO event, When normalizing, Then the entire local day is marked unavailable for that invitee.
Real-Time Sync and Staleness SLA
Given push-capable providers (Google/Microsoft) are connected, When a calendar change notification is received, Then incremental updates are applied and the overlap model reflects the change within 60 seconds end-to-end. Given a non-push iCal/ICS feed, When no push is available, Then the system polls using ETag/Last-Modified and refreshes at least every 15 minutes; the model’s maximum staleness does not exceed 20 minutes. Given transient sync errors or 429 responses, When retries are attempted, Then exponential backoff with Retry-After is honored with up to 3 retries and no data duplication.
Candidate Blocks Computation for Specified Meeting Duration
Given a meeting duration D minutes and N required invitees across time zones, When candidate computation runs, Then it returns the first 50 overlapping free intervals of length D that do not cross any invitee’s blackout dates or fall outside any invitee’s working hours. Given output is requested, When the API returns candidate blocks, Then each block includes start and end in ISO 8601 UTC, plus each invitee’s localized start/end in their IANA timezone. Given events with Busy/Tentative/OOO exist, When computing overlaps, Then any slot intersecting such events for any required invitee is excluded.
Caching, Delta Sync, and Rate Limit Compliance
Given no calendar changes occur, When availability is requested repeatedly within 5 minutes, Then results are served from cache with zero provider API calls. Given providers support delta/sync-token queries, When syncing after an initial full sync, Then only incremental changes are requested and applied; full syncs occur only on token invalidation. Given provider rate limits are approached or 429 is received, When backoff is applied, Then the system stays below rate limits, surfaces a stale cache indicator if data is older than 20 minutes, and completes recovery on the next allowed window.
Overlap Heatmap Visualization
"As a scheduler, I want a visual heatmap of overlapping availability so that I can quickly spot the best meeting windows."
Description

Render a color-coded grid that visualizes overlap density for 2–15 invitees across selected dates and durations, with day and week views. Provide accessible color contrast, keyboard navigation, and responsive design for desktop and mobile. Enable hover/click details to show localized times per invitee, overlap counts, and slot quality score. Allow filters to include/exclude specific invitees, adjust minimum overlap threshold, and pin candidate windows.

Acceptance Criteria
Grid rendering for selected dates, durations, and day/week views
Given 2–15 invitees are selected and a date range and meeting duration are set, When the heatmap loads, Then a color-coded grid renders showing overlap density for the selected timeframe. Given day view is selected, When the duration is 15/30/45/60 minutes, Then the time axis uses increments equal to the selected duration and slots align to those increments. Given week view is selected, When viewing any week within the selected date range, Then the grid shows 7 day columns for that week and time rows aligned to the selected duration. Given the invitee count is fewer than 2 or greater than 15, When attempting to render, Then the UI displays a validation message and disables the grid.
Accurate timezone localization and DST handling in slot details
Given invitees across two or more timezones, When hovering or tapping a slot, Then the details show for each invitee the localized start–end time with timezone abbreviation (e.g., 09:00–09:30 PT). Given a slot that overlaps a DST transition for any invitee, When details are shown, Then the localized times and overlap count reflect the correct DST offset and no invalid or duplicate times are displayed. Given localized times are shown, Then all times are consistent with each invitee’s timezone regardless of the viewer’s timezone.
Accessible color contrast and non-color indicators
Given the heatmap legend and cells are rendered, Then each color step meets WCAG 2.1 AA contrast (≥4.5:1) against the background and adjacent steps. Given the grid is viewed by users who cannot perceive color, Then each cell includes a numeric overlap count visible at 200% zoom without loss of content. Given the legend is visible, Then it includes text labels for minimum, maximum, and intermediate overlap steps.
Keyboard navigation and screen reader support
Given focus is on the grid, When pressing Arrow keys, Then focus moves to the adjacent cell and a screen reader announces date, time range, overlap count, and quality score for the focused cell. Given focus is on the grid, When pressing Home/End, Then focus moves to the first/last cell in the current row; Ctrl+Home/End moves to the first/last cell in the grid. Given a cell is focused, When pressing Enter or Space, Then the slot details open and focus moves into the details; pressing Esc closes the details and returns focus to the originating cell. Given the grid is rendered, Then it uses ARIA roles and attributes (role=grid with row/column headers, aria-selected for pinned/selected cells), tab order reaches all interactive controls, and a visible focus indicator has ≥3:1 contrast.
Responsive layout for desktop and mobile
Given a viewport width ≤ 768px, When opening the heatmap, Then the grid provides horizontal scroll for the time axis, sticky day/date headers, and tap targets measuring at least 44×44 px. Given a viewport width ≥ 1024px, Then the grid fills the container without horizontal overflow and hover tooltips appear within the viewport. Given the device orientation changes, When the grid has a focused/selected cell, Then scroll position and selection are preserved after reflow.
Hover/tap details with overlap count and quality score
Given a user hovers on desktop or taps on mobile a cell with overlap ≥2, Then a details panel shows localized times per invitee, total overlap count, and a slot quality score from 0–100 with a descriptive label (e.g., Good, Excellent). Given the details panel is open, When filters or duration change, Then the panel content updates to reflect new values or closes if the slot no longer meets the threshold. Given the details panel is open, When pressing Esc or tapping outside, Then it closes immediately and returns focus (or scroll context) to the originating cell.
Filters for invitees and minimum overlap, and pinning candidate windows
Given the include/exclude invitees filter is adjusted, When changes are applied, Then the grid recomputes overlap density and the legend and counts reflect only the included invitees. Given the minimum overlap threshold is set to N, When N increases, Then all cells with overlap < N are de-emphasized or hidden and are not selectable; an empty state appears if no cells qualify. Given a user pins a candidate window, Then the cell shows a pinned state, the slot appears in a Pinned list with date/time in the user’s timezone, and the pin persists across day/week views, filter changes, and date navigation; unpin removes it from both grid and list and a maximum of 10 pins is enforced with an error when exceeded.
Fairness Rotation Engine
"As a team lead, I want early/late meetings to rotate fairly so that no one is consistently disadvantaged by time zones."
Description

Implement a scoring and rotation algorithm that distributes early/late meeting times fairly across participants over time. Track per-group history, apply configurable rotation policies (weekly, biweekly), and respect individual constraints and holidays. Incorporate fairness scores into slot ranking, explain rankings via tooltips/logs, and provide admin overrides for exceptions. Persist fairness state to ensure continuity across recurring or future meetings.

Acceptance Criteria
Weekly Fairness Rotation Across Three Time Zones
Given a group of 4 participants in PT, ET, CET, and IST with working hours 09:00–17:00 local and meeting length 60 minutes, and rotationPolicy=weekly, fairnessMetric=cumulative off-hours minutes When generating 8 weekly meeting occurrences starting 2025-10-06 Then the earliest local-time burden rotates to a different participant each occurrence in round-robin order And after 8 occurrences, the standard deviation of off-hours minutes per participant is <= 60 minutes And the fairness ledger (per-participant off-hours total, last-burdened occurrence) is persisted and reused in subsequent runs And if an occurrence is skipped due to no acceptable overlap, the rotation turn carries over to the next scheduled occurrence
Biweekly Rotation Policy Application
Given a group of 3 participants across UTC-8, UTC+0, and UTC+8, rotationPolicy=biweekly, fairnessMetric=cumulative off-hours minutes per 2-week block When generating a recurring series spanning 6 calendar weeks (3 biweekly blocks) Then the same participant may hold the earliest/late burden for two consecutive weeks within a block And the burden rotates on each new block (occurrences 1–2, 3–4, 5–6) in round-robin order And fairness scores accumulate per block and are reflected in ranking and ledger And the UI displays a "Biweekly rotation" label in slot details for affected series
Constraints and Holidays Respected in Scoring and Selection
Given participant-specific constraints (preferred window 10:00–16:00 local, daily max off-hours = 60 minutes, blackout 02:00–05:00 local) and holiday calendars (country + custom) with IANA time zones When evaluating candidate slots for the next 4 weeks Then any slot violating a hard constraint or falling on a participant holiday is excluded (assigned infinite penalty) And slots outside preferred windows but within allowed bounds receive soft penalties used in ranking And no selected slot overlaps 02:00–05:00 local for any participant with that blackout And off-hours calculations honor DST transitions using IANA zones, yielding correct local times and minutes
Fairness Score Weighted Slot Ranking
Given base overlap score S_overlap and fairness score S_fairness per slot, with weights w_overlap=0.6 and w_fairness=0.4 When ranking candidate slots Then the engine computes S_total = (w_overlap*S_overlap) + (w_fairness*S_fairness) for each slot And the top 3 proposed slots are the three highest S_total values And ties on S_total are broken by earlier calendar date, then by lower total off-hours minutes And changing w_fairness from 0.4 to 0.7 reorders the top 3 when fairness differences exist
Transparent Explanations via Tooltips and Audit Log
Given a ranked slot list with computed scores and exclusions When a user hovers a slot Then the tooltip shows per-participant local time, off-hours minutes delta (+/−), current fairness ledger totals, rotation turn owner, and weights used And clicking "View details" opens an audit log entry with participants, constraints, holidays applied, computed scores, and reasons for excluded slots And all timestamps are displayed in both viewer local time and each participant's local time with IANA zone labels
Admin Override Without Penalizing Fairness
Given an admin marks a specific occurrence as "Exception: client constraint" and manually selects a slot that would burden participant P When the meeting is scheduled Then participant P's fairness ledger is not debited for that occurrence And the audit log records the override reason, actor, timestamp, and affected participants And a visible badge "Admin override (no debit)" appears on the scheduled occurrence
New Participant Join/Leave Rebalancing
Given an existing recurring series with 6 weeks of fairness ledger history When a new participant Q joins before week 7 Then Q is initialized at the group median off-hours minutes (rounded to nearest 5 minutes) And the next rotation prioritizes burden assignment to the lowest-ledger participant if constraints allow And when a participant R leaves, their ledger is archived and excluded from future fairness, without altering remaining participants' historical balances
One-Tap Localized Slot Proposals
"As an organizer, I want to propose the top slots with localized times so that invitees can respond quickly without time conversions."
Description

Enable selection of the top-ranked N time slots and generate proposals that display each slot in every invitee’s local time. Share via email and link with RSVP buttons, ICS attachments, and add-to-calendar options. Auto-hold tentative events on the organizer’s calendar, track responses, resolve conflicts on accept, and offer fallback suggestions when a slot becomes invalid. Provide customizable message templates and branding aligned to SoloPilot.

Acceptance Criteria
One-Tap Proposal Generation with Localized Times
Given the organizer has selected participants with known time zones and N top-ranked slots are available in the heatmap And fairness rotation is enabled in workspace settings When the organizer taps "Propose Top N" Then a proposal draft is created containing exactly N distinct slots in the original ranking order And each slot shows, for every invitee, the local date and time per invitee's time zone and locale (12/24‑hour) with DST applied And slot durations match the configured meeting length And the proposal provides both "Send Email" and "Copy Link" options
Email Delivery with RSVP Buttons and ICS per Slot
Given a proposal draft with N slots and invitees with valid email addresses When the organizer sends the proposal via email Then each invitee receives an email within 2 minutes containing each proposed slot with times localized for that invitee And each slot includes Accept, Decline, and Suggest Different Time buttons with unique signed tracking tokens And the email includes Add to Google/Outlook/Apple Calendar options and an .ics attachment per slot with unique UID, SUMMARY, DESCRIPTION, DTSTART/DTEND in UTC, and organizer details And clicking Accept records the response and opens a confirmation page for the invitee And clicking Decline records the response and offers to suggest alternatives
Link Landing Page with Localized Display and RSVP
Given a live proposal link is generated When an invitee opens the link on desktop or mobile Then the page loads in under 3 seconds on a typical 4G connection and displays all proposed slots in the invitee's local time zone and locale with DST applied And the page shows Accept, Decline, and Suggest Different Time controls for each slot And selecting Accept records the response without requiring the invitee to sign in And after acceptance, the page offers Add to Google, Outlook, and iCal options for the chosen slot
Auto-Hold Tentative Events on Organizer Calendar
Given the organizer has a connected calendar and auto-hold is enabled When the proposal is sent Then a tentative Hold event is created on the organizer’s calendar for each proposed slot with Busy status, correct start/end, and a title prefixed with "Hold:" And if any slot is accepted and the meeting is finalized, the corresponding hold converts to a confirmed event and all other holds are removed within 60 seconds And if the proposal is canceled, all holds are removed within 60 seconds
Response Tracking and Notifications
Given a sent proposal with multiple invitees When invitees respond via email buttons or the landing page Then the organizer can view per-invitee response statuses (Accepted slot X, Declined, No response) with timestamps And the system aggregates counts per slot to show which slot has the most accepts And the organizer receives an in-app notification and email when the first acceptance arrives and when all invitees have accepted the same slot
Conflict Handling on Accept with Fallback Suggestions
Given a proposed slot becomes invalid due to organizer calendar conflict or the time passing before acceptance When an invitee attempts to Accept that slot Then the acceptance is rejected with an explanation that the slot is no longer available And the system immediately presents up to three fallback suggestions ranked by overlap quality and fairness rotation, localized for the invitee And selecting a fallback records the response and updates calendar holds accordingly And if no valid alternatives exist, the invitee can submit availability to the organizer instead
Custom Templates and Branding Application
Given the organizer has configured email and landing page templates with logo, brand color, sender name, and placeholders {OrganizerName}, {MeetingTitle}, {SlotList}, and {ResponseLink} When a proposal is generated and sent Then the outbound email and landing page apply the configured branding aligned to SoloPilot styles And all placeholders are rendered with correct values per invitee, including localized slot times And the organizer can preview the email and page before sending And changes to the template persist for future proposals
DST and Timezone Edge-Case Handling
"As an organizer, I want the system to handle DST shifts correctly so that meetings aren’t scheduled at invalid or confusing times."
Description

Integrate a robust timezone library with automatic rules updates to correctly handle daylight saving transitions, historical offsets, and regional anomalies. Detect and warn about ambiguous or nonexistent local times, highlight affected participants, and avoid generating proposals in invalid windows. Include automated tests for major regions and regression checks for upcoming DST changes.

Acceptance Criteria
Exclude nonexistent local times during DST spring-forward
- Given a scheduling window includes a DST start for any participant When generating candidate slots Then all local times that do not exist (skipped hour) for affected participants are excluded from proposals - Given a user interacts with a cell in the skipped hour When the heatmap renders Then the cell is disabled and displays a tooltip/banner indicating "Nonexistent local time due to DST" listing affected participants - Given top overlap slots would have fallen in the skipped hour When auto-selecting top proposals Then the next valid slots are selected instead and the count of excluded slots is recorded in logs
Disambiguate overlapping times during DST fall-back
- Given a scheduling window includes a DST end for any participant When generating candidate slots Then times within the repeated hour are treated as two distinct UTC instants and labeled in local time as first/second occurrence (e.g., 1:30 AM (first), 1:30 AM (second)) - Given a user selects a slot in the repeated hour When proposing to invitees Then each invitee sees a localized time matching the selected UTC instant for their timezone - Given a user clicks an ambiguous local time cell When disambiguation is needed Then the UI requires choosing first or second occurrence before enabling Send
Per-participant DST impact highlighting in Overlap Heatmap
- Given any participant has a DST transition within the selected date range When the heatmap renders Then affected cells for that participant show a distinct DST indicator and ARIA label "DST affected", and the participant is flagged in the sidebar - Given multiple participants have different DST transitions When computing overlap and rendering colors Then overlap colors remain comparable and cells with mixed validity are marked as partially valid with hover details per participant - Given WCAG 2.1 AA requirements When rendering DST indicators Then contrast ratio is >= 4.5:1 and indicators are not color-only dependent
Prevent invalid proposals across DST transitions
- Given the fairness rotation engine computes early/late weeks When a rotation boundary crosses a DST change Then offsets are computed in UTC so no participant is assigned an invalid local time - Given the one-tap "propose top slots" automation runs When top slots intersect nonexistent or ambiguous local times Then invalid slots are skipped and ambiguous slots require disambiguation prior to sending - Given a proposal contains an invalid local time When the user attempts to send Then the API rejects with 422 INVALID_LOCAL_TIME and the UI blocks submission with a clear error message
Automatic timezone rules updates and safe fallback
- Given the platform uses the IANA timezone database When a new tzdata version is released Then a scheduled job detects it within 24 hours, downloads, validates, and activates it without downtime, logging the version change - Given a rules update changes future offsets for a region When generating proposals for dates beyond the change Then new offsets are used and a "rules updated" notice is recorded in audit logs - Given update validation fails When activation is attempted Then the previous tzdata remains active, on-call is alerted, and retries occur with exponential backoff
Automated tests for major regions and upcoming DST changes
- Given nightly CI runs When executing the DST regression suite Then tests cover US/Pacific, US/Eastern, Europe/Berlin, Europe/London, Australia/Sydney, America/Sao_Paulo, Africa/Casablanca, and Asia/Jerusalem for spring-forward and fall-back cases, all passing - Given the calendar is within 90 days of any supported region's DST change When the suite runs Then auto-generated test cases for those regions are included and failures trigger paging to on-call - Given a timezone library or tzdata version is updated When unit and property-based tests run Then no regressions > ±1 second are detected against baseline snapshots
Privacy and Availability Permissions
"As a privacy-conscious user, I want my detailed calendar contents hidden so that only free/busy data is used for overlap calculations."
Description

Use least-privilege, read-only OAuth scopes to ingest only free/busy data and timezone information unless explicit content access is granted. Encrypt data at rest and in transit, provide a consent screen detailing data usage, and support manual availability input for users who cannot connect calendars. Offer per-invitee visibility controls, audit logs for data access, and one-click disconnection with data deletion to meet compliance expectations.

Acceptance Criteria
Read-only OAuth Scopes: Free/Busy & Timezone Only
Given a user connects a calendar provider, When the OAuth consent screen is displayed, Then only read-only scopes required to retrieve free/busy windows and timezone are requested. Given the connection is completed, When SoloPilot calls the provider, Then only endpoints returning free/busy and timezone metadata are invoked and responses with event content (titles, descriptions, attendees, locations) are neither requested nor stored. Given the provider requires broader scopes for connection, When the user attempts to authorize, Then the connection is blocked and the user is prompted to re-authorize with least-privilege scopes. Given the Overlap Heatmap renders, Then no event content is present in the UI or network payloads; only availability blocks and localized times are transmitted.
Consent & Scope Escalation Flow
Given a user starts calendar connection, When the in-app consent screen is shown, Then it lists data types (free/busy, timezone), purpose (Overlap Heatmap), retention policy, sharing policy, and links to Privacy Policy/DPA, with Continue and Cancel options. Given the user selects Cancel on the consent screen, Then no OAuth request is initiated and no calendar data is stored. Given a feature requires event content access, When the user triggers it, Then an escalation dialog explains the additional data types and purpose and requires explicit consent before requesting new scopes. Given the user denies the escalation, Then the feature remains disabled and no additional scopes are requested. Given the user revokes previously granted elevated access in settings, Then elevated scopes are revoked and any stored content fetched under those scopes is deleted within 24 hours, retaining only minimal audit metadata.
Encryption In Transit and At Rest
Given any client-server or server-to-provider communication, Then TLS 1.2+ is enforced and connections with invalid certificates are rejected. Given calendar tokens and cached free/busy/timezone data are stored, Then they are encrypted at rest using strong encryption (e.g., AES-256 or cloud KMS equivalent) and are unreadable in database snapshots. Given application logs are produced, Then access/refresh tokens and event content are never logged and are redacted in error traces. Given key management is configured, Then KMS-backed keys with rotation enabled are used and verifiable via environment configuration.
Manual Availability Input Support
Given a user opts not to connect a calendar, When they open Availability settings, Then they can set timezone, recurring working hours, and one-off busy/available overrides. Given manual availability is configured, When the Overlap Heatmap is generated, Then the heatmap computes overlaps using the manual data with parity to connected calendars. Given manual availability is used, Then no calls to external calendar providers are made for that user. Given the user updates their manual timezone or overrides, Then proposed slots and localized times update immediately upon refresh.
Per-Invitee Visibility Controls
Given an organizer creates a heatmap session, When adding invitees, Then the organizer can set visibility per invitee to one of: Aggregated overlap only; Show my free/busy blocks only; Hide my availability. Given visibility is set to Aggregated overlap only, Then invitees see only the color-coded overlap grid with localized times and cannot view any individual’s calendar details or identities beyond the invite list. Given visibility is set to Hide my availability, Then the organizer’s availability is excluded from others’ view while remaining available to the organizer privately. Given any visibility setting, Then event titles, descriptions, locations, and attendee lists from calendars are never visible to invitees.
Audit Logging of Calendar Data Access
Given any retrieval of calendar free/busy or timezone data, Then an audit log entry is created within 5 seconds capturing actor/service, user account, provider, scopes used, purpose (Overlap Heatmap), timestamp, request ID, and success/failure without storing event content. Given an admin queries audit logs for a time range, Then entries are immutable and retrievable with filtering by user, provider, and purpose. Given a user requests an access report, Then a downloadable log of their calendar data accesses for the last 90 days is available within 5 minutes.
One-Click Disconnection and Data Deletion
Given a user has a connected calendar, When they click Disconnect in Integrations, Then provider access/refresh tokens are revoked and removed from storage and the UI confirms disconnection within 10 seconds. Given disconnection completes, Then cached free/busy and timezone data for that calendar are deleted within 15 minutes, and future heatmaps for that user rely solely on manual availability unless they reconnect. Given a disconnection event occurs, Then an audit log entry is created capturing actor, timestamp, provider, and deletion completion time. Given the user reconnects after disconnection, Then no prior calendar data persists or pre-populates the system.
SoloPilot Session Workflow Integration
"As a SoloPilot user, I want confirmed meetings to become sessions that feed invoicing so that I avoid manual handoffs and missed billing."
Description

On confirmation of a selected slot, auto-create a SoloPilot session linked to the client or group profile, attach the agreed time in each participant’s local timezone, and trigger existing automations (reminders, notes templates, and session-to-invoice flows). Support rescheduling with propagation to proposals and sessions, emit webhooks for downstream automations, and update billing artifacts to prevent missed charges.

Acceptance Criteria
Auto-create Session on Slot Confirmation
Given a confirmed meeting slot originating from an Overlap Heatmap proposal and including service, participants, and proposal_id When the organizer confirms the selected slot Then SoloPilot creates exactly one session record with status "Scheduled" within 2 seconds And the session is linked to the correct client_id or group_id based on the proposal And the session stores start_at_utc and end_at_utc matching the confirmed slot and the selected duration And the session records service_id, rate, organizer_id, and proposal_id And repeat confirmations with the same proposal_id and slot are idempotent and do not create duplicates And an audit event "session.created" is recorded with actor_id, session_id, and timestamp
Attach Localized Times to All Participants
Given a session with participants in 2 or more timezones When the session is created or rescheduled Then each participant sees the start and end time localized to their IANA timezone (e.g., "America/Los_Angeles") in notifications and UI And the session stores canonical UTC timestamps and a participant_timezones array mapping participant_id -> timezone And calendar invitations render correct local start/end in Google Calendar and Outlook for test accounts in at least 3 distinct DST-observing regions and 1 non-DST region And times crossing DST boundaries match UTC conversion with no ±60 minute drift in test cases And if a participant has no saved timezone, the system defaults to the organizer’s timezone and flags the record with timezone_source = "defaulted"
Trigger Reminders, Notes Template, and Session-to-Invoice Automations
Given tenant-level automations are enabled for reminders, notes templates, and session-to-invoice When a session is created with status "Scheduled" Then reminder jobs are scheduled per configuration (e.g., 24h and 1h before start) with no duplicate jobs for the same session and offset And the configured notes template is attached to the session and visible to the organizer within 5 seconds And the session-to-invoice flow is primed so that when the session status transitions to "Completed" an invoice draft is created and linked within 10 seconds And if automations are disabled at the tenant or client level, no jobs or drafts are created and an audit entry notes the skip reason And all scheduled jobs and artifacts are logged under the session activity feed
Rescheduling Propagates to Proposals, Session, Invites, and Reminders
Given a Scheduled session linked to a proposal and calendar invite When the organizer selects a new slot and confirms reschedule Then the session’s start_at_utc and end_at_utc update and the session version increments by 1 And the originating proposal and any outstanding proposals reflect the updated proposed time and are marked "Updated" And prior reminder jobs are canceled and replaced with jobs aligned to the new start time with no residual jobs remaining And the calendar invite is updated with the same UID so attendees receive an update, not a new invite And participants receive reschedule notifications showing localized new times And the system prevents double-booking conflicts and returns a clear error if the new slot overlaps an existing session for the organizer And an audit event "session.rescheduled" is recorded with old/new times and actor
Emit Webhooks for Key Session and Billing Events
Given a tenant has an active webhook endpoint and secret When a session is created, updated, rescheduled, canceled, or completed, or when billing artifacts are created/updated Then the system emits events: session.created, session.updated, session.rescheduled, session.canceled, session.completed, billing.artifact.created, billing.artifact.updated And each payload includes idempotency_key, event_type, occurred_at, session_id, client_id or group_id, start_at_utc, end_at_utc, status, and for billing events: invoice_id or credit_note_id and amount And webhooks are signed with HMAC-SHA256 in header "X-SoloPilot-Signature" and include "X-SoloPilot-Delivery-Id" And delivery occurs within 5 seconds of the triggering action with exponential backoff retries for up to 24 hours on non-2xx responses And 2xx responses are treated as success; 4xx (except 429) stop retries; 5xx and 429 are retried And duplicate deliveries carry the same idempotency_key for de-duplication by receivers
Update Billing Artifacts to Prevent Missed or Duplicate Charges
Given a session has an associated service, rate, and billing rules When the session status transitions to "Completed" and no invoice exists Then an invoice draft is created with a line item linked to the session_id, correct rate/quantity/duration, and applicable taxes/discounts And if an invoice already exists for the session, no duplicate invoice is created; instead the existing invoice line item is updated if the duration or rate changed prior to completion And when a session is canceled inside the configured late-cancellation window, a cancellation-fee invoice draft is created per policy; outside the window no fee is billed And when a reschedule occurs after invoicing, the system creates an adjustment (credit/debit note) rather than double-billing, and links the adjustment to the original invoice and session And all billing artifacts maintain a back-reference to session_id to ensure end-to-end traceability

Dual‑Time Stamps

Adds clear, audit‑friendly time stamps to invoices, receipts, and session summaries showing both parties’ local time, timezone abbreviation, and UTC. Uses each client’s local date format to avoid disputes and supports compliance exports with a consistent reference clock.

Requirements

Per-Party Timezone & Locale Resolution
"As a solo provider, I want SoloPilot to know my clients’ local timezones and date formats so that documents and timestamps match what they expect without manual adjustments."
Description

Resolve and store each party’s canonical IANA timezone (e.g., America/Los_Angeles) and locale preferences at the workspace (provider) and client levels. Provide automatic detection on invite/first booking, explicit override in profile, and fallbacks for unknown timezones. Persist the timezone ID, locale, 12/24‑hour preference, and date format per user to drive document rendering, scheduling, and exports. Track the TZDB version used for calculations to ensure reproducibility and auditability across DST changes and historical rule updates.

Acceptance Criteria
Auto-Detect Timezone and Locale on First Interaction
Given a new client opens an invite or first booking page When the page loads and the client submits the booking/intake form Then the system captures the client’s IANA timezone from the browser and stores it exactly (e.g., "America/Los_Angeles") And the system captures the client’s locale (e.g., "en-US") and stores it as a BCP 47 tag And the system infers a 12/24‑hour preference from the locale default and stores it And the system infers a locale-appropriate date format and stores it And the detection source is recorded as "auto" with a timestamp And the TZDB version used for any offset calculations during detection is recorded (e.g., "2025a")
Explicit Profile Override of Timezone and Locale
Given a user (provider or client) opens Profile > Preferences When they set Timezone to a valid IANA zone via search-select And they set Locale to a valid BCP 47 tag And they toggle 12/24‑hour and choose a date format option And they save changes Then the system validates values (IANA zone exists in current TZDB, locale is well-formed) And persists timezoneId, locale, timeFormat, dateFormat, updatedBy, updatedAt And subsequent renders and scheduling calls read from the stored values And an audit entry is created recording old -> new values and actor
Fallback Strategy for Unknown or Inconsistent Timezone
Given timezone/locale detection fails or returns an invalid value When the client completes booking or the profile lacks valid preferences Then the system sets timezoneId to the workspace default timezone And sets locale to the workspace default locale And marks source as "fallback-workspace" And prompts the user on next sign-in to confirm or change preferences And all documents and scheduling use the fallback values until overridden And the event is logged with reason "detection-failed" and TZDB version recorded
Persist Per-User Preferences and Retrieval
Given a user has stored timezoneId, locale, timeFormat, and dateFormat When they sign in on any device or an API session is created Then the system retrieves the exact stored values within 200 ms p95 And values persist across sessions and devices And values are accessible to document rendering, scheduling, and exports via a single read API And values are included in backups and restore correctly during environment rehydrate
Dual-Time Rendering on Financial and Session Documents
Given a session occurred at a specific UTC timestamp When an invoice, receipt, or session summary is generated Then the document shows the provider’s local date/time using the provider’s stored timezoneId, dateFormat, and 12/24‑hour preference with timezone abbreviation And the document shows the client’s local date/time using the client’s stored timezoneId, locale-specific date format, and 12/24‑hour preference with timezone abbreviation And the document includes the UTC timestamp in ISO 8601 (e.g., 2025-09-22T17:30:00Z) And all three times resolve from the same recorded TZDB version And a unit test verifies formatting for at least en-US (12h) and en-GB (24h)
Persist TZDB Version for Reproducibility
Given any time conversion or rendering is performed When offsets and abbreviations are computed Then the exact TZDB version string (e.g., "2025a") used is recorded with the stored result or document metadata And re-rendering the same document later uses the recorded TZDB version to reproduce the original offsets and abbreviations And if the runtime TZDB differs, the system logs "tzdb-version-mismatch" and warns in internal telemetry without altering the published document
Compliance Exports with Reference Clock and Locales
Given a compliance export is requested for a date range When the export is generated Then every row includes UTC timestamp, provider local timestamp, client local timestamp, timezone abbreviations, timezoneIds, locales, and the TZDB version used And timestamps reflect offsets from the recorded TZDB version across DST boundaries And date formats in export follow each party’s stored preferences for display columns while UTC is ISO 8601 And the export passes schema validation and can be re-imported to reproduce original times
Dual Timestamp Rendering on Documents
"As a consultant, I want every invoice and session summary to show my client’s local time alongside mine and UTC so that there is no confusion about when services occurred or were billed."
Description

Render both parties’ local timestamps and UTC on invoices, receipts, and session summaries. For each relevant datetime (session start/end, invoice issued, payment received), display: provider local time with timezone abbreviation and offset, client local time with timezone abbreviation and offset, and UTC in ISO 8601. Place timestamps consistently in document templates and email previews, ensure responsive layout, and support PDF generation. Labels must clearly indicate which party the time refers to and avoid collisions with existing fields. Copy-to-clipboard for UTC value.

Acceptance Criteria
Invoice Issued Timestamp: Dual-Render (Web/Email/PDF)
Given a provider timezone and a client timezone and an invoice with an issued_at value When the invoice is viewed in the web app, in the email preview, and exported to PDF Then each surface renders, directly under the "Invoice Issued" field, three labeled timestamps in this order: Provider local, Client local, UTC And each local timestamp includes the local date, local time, timezone abbreviation, and numeric UTC offset And the UTC timestamp is ISO 8601 in UTC with Z suffix (e.g., 2025-09-22T16:30:00Z) And labels clearly identify which party each timestamp refers to And the timestamp content and order are identical across web, email preview, and PDF for the same invoice
Payment Received Timestamp: Dual-Render with DST Accuracy
Given a receipt with payment_received_at and distinct provider/client time zones When the receipt is rendered in the web app, in the email preview, and exported to PDF Then for payment_received_at, three timestamps appear in the order: Provider local, Client local, UTC, each with timezone abbreviation and numeric UTC offset And the local abbreviations and offsets reflect the exact instant of payment (honors daylight saving transitions for each zone) And a predefined fixture set that straddles DST boundaries renders the expected abbreviations and offsets per the IANA tz database And labels and placement are consistent with the invoice implementation
Session Start/End: Dual-Render and Duration Integrity
Given a session with start_at and end_at times and distinct provider/client time zones (including cross-midnight cases) When the session summary is viewed in the web app, email preview, and exported to PDF Then both start and end show three timestamps each (Provider local, Client local, UTC), each with abbreviation and numeric offset And the session duration displayed equals end_at − start_at computed in UTC to the minute And local date boundaries (day/month/year changes) are correctly reflected in each local timestamp without altering the computed duration And order, labels, and placement are consistent with invoices and receipts
Timezone Abbreviation and Offset Formatting Rules
Rule: Local timestamps display as "<local date> <local time> <TZ abbreviation> (UTC±HH:MM)" (e.g., 15/10/2025 18:30 CEST (UTC+02:00)) Rule: Abbreviation and offset are calculated for the exact instant shown (includes DST where applicable) Rule: If the timezone abbreviation for an instant is unavailable, display only the numeric offset as (UTC±HH:MM) Rule: Abbreviation capitalization matches the canonical form for that zone/instant (e.g., PST, PDT, CET, CEST) Rule: No local timestamp is shown without an accompanying numeric UTC offset
Local Date Formats for Client and Provider Timestamps
Given client profile locales of en-GB, en-US, and de-DE When rendering Client local timestamps Then the date portion formats respectively as DD/MM/YYYY, MM/DD/YYYY, and DD.MM.YYYY And Provider local timestamps use the provider’s profile locale date format And UTC timestamps always use ISO 8601 date format regardless of locale And unit tests confirm the correct date ordering for at least these locales and a default fallback
Responsive Layout and Field Non-Collision Across Surfaces
Given viewports of 320px, 768px, and 1440px widths in the web app, and email preview widths of 375px and 600px When viewing invoices, receipts, and session summaries Then all three timestamps for each relevant field are visible, may wrap within their container, and do not overlap or truncate adjacent fields (e.g., Due Date, Total, Balance) And timestamps are positioned directly beneath their related field label in all templates and maintain the same order (Provider, Client, UTC) And exported PDFs (A4 and US Letter) render timestamps fully within page margins without clipping or overlapping other content
UTC ISO 8601 Rendering and Copy-to-Clipboard
Given any document surface where a UTC timestamp is displayed When the user clicks the copy control adjacent to the UTC timestamp or activates it via keyboard (Enter/Space) Then the exact ISO 8601 UTC string (YYYY-MM-DDTHH:mm:ssZ) for that field is copied to the clipboard And a non-blocking confirmation message appears within 2 seconds (e.g., "UTC copied") and dismisses automatically within 4 seconds And if the Clipboard API is unavailable or permission is denied, the UTC string is selected for manual copy and an instructional message is shown And the copy control is keyboard focusable, has an accessible name (e.g., "Copy UTC to clipboard"), and is announced by screen readers
Locale-Aware Date and Time Formatting
"As a client viewing an invoice, I want dates and times displayed in my familiar format so that I can quickly verify details without misreading them."
Description

Format dates and times using the client’s local conventions (e.g., DD/MM/YYYY vs MM/DD/YYYY, 24‑hour vs 12‑hour) while preserving a consistent reference clock (UTC). Provide a formatting engine that maps locale preferences to patterns across documents, emails, and portal views. Ensure punctuation, month names, and separators align with the client’s locale. Validate inputs and prevent ambiguous representations by pairing localized times with explicit timezone abbreviations and offsets.

Acceptance Criteria
Invoice Dual-Time Rendering per Client Locale
Given a client profile with locale = en-GB and timezone = Europe/London, and a workspace owner with locale = en-US and timezone = America/Los_Angeles When an invoice is generated for a session occurring at a specific instant Then the invoice displays three time stamps: Client Local, Owner Local, and UTC And Client Local is formatted as dd/MM/yyyy HH:mm with the correct timezone abbreviation and numeric UTC offset for the event instant And Owner Local is formatted as MM/DD/YYYY h:mm a with the correct timezone abbreviation and numeric UTC offset for the event instant And UTC is displayed using a consistent reference format (ISO 8601 with Z or "YYYY-MM-DD HH:mm UTC") And all three time stamps represent the same instant in time
Email Booking Confirmation Uses Client Locale and UTC
Given a scheduled session notification email destined for a client with locale = fr-FR and timezone = Europe/Paris When the email is sent Then the session date/time in the email body is formatted per fr-FR conventions (dd/MM/yyyy HH:mm and localized month/day names in long-form where used) And the line includes the timezone abbreviation and numeric UTC offset applicable at the event instant (e.g., CET +01:00 or CEST +02:00) And the email also includes a UTC reference time for the same instant in ISO 8601 (e.g., 2025-03-02T14:05:00Z) And no numeric-only MM/DD vs DD/MM ambiguity appears anywhere in the email
Client Portal Session Summary Shows Dual Local Times and UTC
Given a client viewing a session summary in the portal with locale = de-DE and timezone = Europe/Berlin, and an owner timezone = America/New_York When the summary is rendered Then the summary displays Client Local, Owner Local, and UTC for the session timestamp And Client Local is formatted as dd.MM.yyyy HH:mm with German separators, plus timezone abbreviation and numeric offset (e.g., CET/CEST) And Owner Local is formatted per en-US conventions (MM/DD/YYYY h:mm a) with timezone abbreviation and numeric offset (e.g., EST/EDT) And UTC is displayed using ISO 8601 Z format And all three values align to the same underlying instant
Centralized Formatting Engine Maps Locales Across Surfaces
Given supported locales en-US, en-GB, fr-FR, and de-DE configured in the formatting engine When the client’s locale preference is changed on their profile Then subsequent invoices, receipts, session summaries, scheduling emails, and compliance exports all use the updated locale patterns without per-surface overrides And en-US times use MM/DD/YYYY and h:mm a; en-GB uses dd/MM/yyyy and HH:mm; fr-FR uses dd/MM/yyyy and HH:mm with French month names in long-form; de-DE uses dd.MM.yyyy and HH:mm And month names, day names, and punctuation/separators match the locale (e.g., "mars" in fr-FR, dot separators in de-DE) And the same engine also outputs a canonical UTC value in ISO 8601 for every surface
Manual Date/Time Input Validation Prevents Ambiguity
Given a date/time input field bound to the client’s locale pattern When the user types a value that does not conform to the locale’s expected pattern (e.g., using MM/DD in en-GB) Then the field shows an inline validation error explaining the required pattern and prevents save And when the typed value could be ambiguous (day and month <= 12), the parser applies the client’s locale rules and immediately shows a parsed preview with timezone abbreviation and numeric offset for the selected timezone And no record can be saved unless a timezone context is present and the rendered preview includes an explicit timezone abbreviation and offset And keyboard input and date/time picker selections produce identical stored instants and formatted outputs
DST-Aware Abbreviations and Offsets Are Correct
Given a session scheduled in a timezone that observes DST When viewing timestamps that fall before and after the DST transition in that region Then the displayed timezone abbreviation changes appropriately (e.g., PST ↔ PDT, CET ↔ CEST, GMT ↔ BST) And the numeric UTC offset matches the abbreviation for the exact instant And Client Local, Owner Local, and UTC displays remain synchronized to the same instant across the transition
Compliance Export Includes Localized and UTC Columns
Given a request to export invoice and session records for a client with locale = en-GB When the CSV export is generated Then each timestamped record includes three columns: client_local, owner_local, and utc And client_local is formatted with the client’s locale pattern and includes timezone abbreviation and numeric offset And owner_local is formatted with the owner’s locale pattern and includes timezone abbreviation and numeric offset And utc is provided in ISO 8601 (YYYY-MM-DDTHH:mm:ssZ) And all three columns reference the same instant without rounding or drift
DST and Historical Rule Accuracy
"As a therapist, I want timestamps to stay accurate across daylight saving changes so that session times and invoices remain correct and defensible in audits."
Description

Use the IANA time zone database to compute accurate offsets for historical and future dates, correctly handling daylight saving transitions, skipped or repeated hours, and political rule changes. Store source UTC timestamps and zone IDs with each record to enable deterministic recalculation. When an event falls in an ambiguous window, annotate the display with the applicable offset and transition note. Include the TZDB version used, and provide a safe upgrade path to recompute displays without altering the original event time.

Acceptance Criteria
Persist UTC, Zone ID, and TZDB Version per Record
Given a new session is saved with local zone "America/Los_Angeles" and computed event_utc "2025-02-14T18:10:00Z" When the record is persisted Then it stores event_utc exactly "2025-02-14T18:10:00Z", zone_id exactly "America/Los_Angeles", and tzdb_version equals the platform's TZDB version string And event_utc and zone_id are immutable on subsequent updates And all three fields are retrievable via API and export
Ambiguous Hour Annotation on DST End (Repeated Hour)
Given event_utc "2024-11-03T05:30:00Z" and zone_id "America/New_York" When rendering a dual-time stamp Then the local time shows "01:30" with abbreviation "EDT" and numeric offset "-04:00" And the display includes an annotation indicating "DST end" and "repeated hour" and specifies "applied offset -04:00" And the UTC time shows "2024-11-03 05:30 UTC+00:00" And the client's local date format is applied to the local timestamp
No Nonexistent Local Times on DST Start (Skipped Hour)
Given zone_id "America/New_York" When rendering event_utc "2025-03-09T06:30:00Z" Then the local time shows "01:30" with abbreviation "EST" and offset "-05:00" When rendering event_utc "2025-03-09T07:15:00Z" Then the local time shows "03:15" with abbreviation "EDT" and offset "-04:00" And no "02:xx" local times are produced for that zone on 2025-03-09 And dual-time stamps include UTC and the zone abbreviation
Deterministic Recalculation with Stored UTC and Zone ID
Given a saved record with event_utc X and zone_id Y and tzdb_version V1 When recalculating the local display using TZDB version V1 Then the output local time, abbreviation, offset, and annotations are identical to the original rendering When recalculating the local display using a different TZDB version V2 Then the output local time equals the mapping of X in zone Y under V2 and event_utc remains unchanged And repeated recalculations with V2 produce identical outputs
Safe TZDB Upgrade and Recompute Path
Given the system currently uses TZDB version V1 and TZDB version V2 is installed When an administrator triggers the "Recompute displays with V2" job Then all affected records recompute derived display fields and annotations using V2 without modifying event_utc or zone_id And each recomputed record is audit-logged with previous tzdb_version V1 and new tzdb_version V2 and any changed offsets And the operation is idempotent and resumable; rerunning produces no further changes
Compliance Export with UTC Reference and TZ Metadata
Given a compliance export is generated for a date range When the file is produced Then each row contains event_utc (ISO 8601 Z), zone_id (IANA), tzdb_version used for rendering, local_time string with timezone abbreviation and numeric offset, and UTC time And offsets and abbreviations in the export match those shown in the app for the same records And exported times remain correct across DST transitions and historical dates by validating against the TZDB library for a sample of N≥50 records
Compliance-Ready Exports and API/Webhook Fields
"As a finance admin, I want exports and API/webhook data to include dual timestamps and UTC so that our accounting system and auditors can reconcile records without timezone errors."
Description

Extend CSV/PDF exports, public API responses, and webhooks to include provider local time, client local time, and UTC for all time-bearing records, along with timezone abbreviations, IANA zone IDs, numeric offsets, and TZDB version. Use ISO 8601 for UTC fields and stable, documented column/field names. Ensure exports are consistent across bulk and single-record endpoints and that downstream systems can reliably parse and reconcile records for audits and accounting.

Acceptance Criteria
CSV Export: Dual-Time and TZ Metadata Columns
Given a workspace with at least one invoice, receipt, and session containing time-bearing fields When the user exports those records to CSV via the bulk export tool Then each CSV row for a time-bearing record contains all of the following columns with non-empty values: provider_local_time_display, provider_local_time_iso, provider_tz_abbrev, provider_tz_iana, provider_utc_offset, client_local_time_display, client_local_time_iso, client_tz_abbrev, client_tz_iana, client_utc_offset, utc_time_iso, tzdb_version And utc_time_iso is in ISO 8601 UTC format (e.g., 2025-09-22T15:30:00Z) And provider_local_time_display and client_local_time_display use the provider’s and client’s configured local date/time formats respectively And provider_local_time_iso and client_local_time_iso are ISO 8601 timestamps including numeric offset (e.g., 2025-09-22T08:30:00-07:00) And provider_tz_iana and client_tz_iana are valid IANA zone IDs (e.g., America/Los_Angeles) And provider_tz_abbrev and client_tz_abbrev reflect the correct abbreviation for the instant (e.g., PDT) And provider_utc_offset and client_utc_offset are formatted as ±HH:MM and match the offset embedded in the corresponding _local_time_iso values And tzdb_version is present and matches the runtime TZDB version string (e.g., 2025a) And the column names exactly match those listed above and remain stable across repeated exports during the same release
PDF Export: Dual-Time Display with Locale and UTC Reference
Given invoices and receipts with time-bearing fields for providers and clients in different time zones When the user exports PDFs for those records Then each PDF visibly shows three labeled timestamps for each time-bearing field: Provider Local (with tz abbreviation and IANA ID), Client Local (with tz abbreviation and IANA ID), and UTC (ISO 8601 with trailing Z) And the Provider Local and Client Local timestamps are rendered using each party’s configured local date/time format And the UTC timestamp is rendered as ISO 8601 UTC (e.g., 2025-09-22T15:30:00Z) And a TZDB version string (e.g., 2025a) is present in the PDF (footer or metadata) for audit traceability And the values shown in the PDF match the corresponding CSV and API values for the same records
Public API: Field Consistency Across Bulk and Single Endpoints
Given public API endpoints for listing and fetching single records (e.g., GET /api/invoices and GET /api/invoices/{id}) When responses are returned for time-bearing records (invoices, receipts, sessions) Then each record contains the following fields with identical names, types, and semantics in both list and single-record responses: provider_local_time_iso, provider_tz_abbrev, provider_tz_iana, provider_utc_offset, client_local_time_iso, client_tz_abbrev, client_tz_iana, client_utc_offset, utc_time_iso, tzdb_version And utc_time_iso is ISO 8601 UTC with Z suffix And *_local_time_iso fields include the numeric offset and are valid ISO 8601 And all fields are non-null for time-bearing records And the same schema applies consistently across invoices, receipts, and session-related endpoints And field ordering and naming remain stable across minor releases
Webhook Payload: Parity with API and Schema Validation
Given webhook events for time-bearing records (e.g., invoice.created, receipt.paid, session.completed) When a webhook is delivered to a subscribed endpoint Then the JSON payload includes the same time fields and names as the public API: provider_local_time_iso, provider_tz_abbrev, provider_tz_iana, provider_utc_offset, client_local_time_iso, client_tz_abbrev, client_tz_iana, client_utc_offset, utc_time_iso, tzdb_version And the values in the webhook payload match the values returned by the corresponding GET API for the same record at delivery time And the payload validates against the published webhook JSON schema without warnings or errors And all time fields are non-null for time-bearing records
DST and Ambiguous Time Handling at Transitions
Given a session scheduled at a daylight saving time transition in the client’s locale (one just before the jump forward and one during the fall-back hour overlap) When the records are exported via CSV, fetched via API, and delivered via webhook Then the client_local_time_iso and provider_local_time_iso reflect the correct offset and timezone abbreviation for the exact instant And utc_time_iso maps exactly to those local times (no off-by-one-hour discrepancies) And numeric offsets (±HH:MM) correctly represent the instant even for half-hour or 45-minute offset zones And downstream parsing of *_local_time_iso and conversion to UTC reproduces the exact utc_time_iso value
ISO 8601 and Timezone Metadata Formatting Compliance
Given any time-bearing record across invoices, receipts, and sessions When inspecting the time fields in CSV, API responses, and webhook payloads Then utc_time_iso strictly matches the pattern YYYY-MM-DDTHH:MM:SSZ (optionally allowing fractional seconds per ISO 8601 extended format) And *_local_time_iso strictly match ISO 8601 with numeric offset (YYYY-MM-DDTHH:MM:SS±HH:MM) And provider_utc_offset and client_utc_offset are formatted as ±HH:MM and match the offsets embedded in *_local_time_iso And provider_tz_iana and client_tz_iana are valid canonical IANA zone IDs present in the declared tzdb_version And provider_tz_abbrev and client_tz_abbrev are correct for the instant and locale (e.g., EST vs EDT)
Cross-Channel Consistency and Reconciliation for Audits
Given the same set of records retrieved via API (bulk and single), exported as CSV, and received via webhooks When comparing time fields across all channels for each record Then utc_time_iso values are identical across channels And converting provider_local_time_iso and client_local_time_iso to UTC yields the same utc_time_iso within a tolerance of 1 second And invoice, receipt, and session records that reference the same event share the same utc_time_iso And no required time or timezone metadata fields are missing or null across channels And a downstream import script can parse all delivered fields without errors and reconcile records solely using utc_time_iso and IDs
Historical Backfill and Migration Utility
"As an account owner, I want to retroactively apply dual timestamps to past records so that our archive remains consistent and audit-ready."
Description

Provide a background job to backfill dual timestamps for existing sessions, invoices, and payments using stored UTC times and the correct historical timezone rules at the event date. Offer scope selection (all, by date range, by client), progress reporting, retry on failure, and idempotency. Log changes with before/after snapshots for audit. Avoid performance degradation by batching and rate-limiting document re-rendering and PDF regeneration.

Acceptance Criteria
Backfill Correctness Using Historical Timezone Rules
Given existing sessions, invoices, and payments with stored event UTC timestamps and provider/client timezones as of the event date When the backfill job is executed Then dual timestamps are computed using IANA timezone rules effective on the event date, correctly handling DST gaps and overlaps And provider local time and client local time are stored with timezone abbreviations (e.g., PDT) and the client’s locale-specific date format And UTC time is stored in ISO 8601 format with Z designator And regenerated invoices/receipts/session summaries display both local times and UTC And compliance exports reflect the same UTC reference clock and matching local times
Scoped Execution: All, Date Range, and Client Filters
Given scope = All When the job runs Then all eligible historical sessions, invoices, and payments are processed Given scope = Date Range (start and end inclusive) When the job runs Then only records with event_at within the inclusive range (evaluated in UTC) are processed Given scope = Client Filter (one or more client IDs) When the job runs Then only records associated to the selected clients are processed Given multiple scopes are combined (e.g., Date Range + Client Filter) When the job runs Then only records matching all active filters are processed And the final processed count equals the number of eligible records for the selected scope(s)
Progress Visibility for Long-Running Backfill Job
Given a backfill job is running When the progress endpoint/UI is polled Then it returns total_count, processed_count, succeeded_count, failed_count, queued_count, percent_complete, current_batch_number, and ETA And these metrics update at least every 5 seconds while work is in progress And on completion, a summary with counts, start/end timestamps, duration, and job status (Succeeded/Completed with Failures/Failed) is persisted and accessible
Automatic Retry and Failure Isolation
Given a record fails due to a transient error (e.g., network timeout, 429, renderer crash) When the job processes that record Then it retries up to 3 times with exponential backoff and jitter And if all retries fail, the record is marked failed, the error is logged with stack/context, and processing continues with the next record And partial writes are rolled back or overwritten to avoid inconsistent state
Idempotent Re-runs and Conditional Regeneration
Given a record already has correct dual timestamps matching recomputed values When the job is re-run with the same scope and parameters Then the record is skipped without modifying data And associated PDFs are not regenerated Given the job is executed multiple times over the same scope without changes in inputs When comparing run summaries Then the results are identical (same processed/succeeded/failed counts) and no duplicate audit entries are created
Comprehensive Audit Logging with Before/After Snapshots
Given the job updates a record When the change is committed Then an audit log entry is created capturing record_id, entity_type, event_at_utc, job_id, actor=system, timestamp, and before/after snapshots of the affected timestamp fields And audit entries are immutable, queryable by job_id and record_id, and exportable in CSV/JSON And failed records also produce audit entries capturing error details without after snapshots
Performance Safeguards via Batching and Rate Limiting
Given default settings batch_size=500, max_concurrency=2 workers, and pdf_regen_rate_limit=3 documents/second/worker When the backfill job runs under typical production load Then p95 latency of core user endpoints degrades by no more than 15% relative to the 5-minute pre-job baseline And average app CPU utilization remains below 70% and DB CPU below 60% And document re-rendering and PDF regeneration are rate-limited to the configured thresholds And if thresholds are exceeded, the job auto-throttles by reducing concurrency and inserting inter-batch delays until metrics are within limits
Admin Controls and Display Preferences
"As a solo operator, I want to control how dual timestamps appear by default and per document so that I can meet client expectations and compliance needs without extra work."
Description

Add workspace-level defaults and client-level overrides to enable/disable dual timestamps, choose which elements to display (provider, client, UTC), select format styles, and configure labels. Provide per-document toggles in the editor with live preview. Ensure settings propagate to emails, client portal views, and PDFs. Include guardrails to prevent disabling UTC on compliance-required exports.

Acceptance Criteria
Workspace Defaults: Dual‑Timestamp Elements and Formats
Given a workspace admin enables Dual‑Time Stamps and selects Provider, Client, and UTC with default labels and formats When a new invoice, receipt, or session summary is generated for a client without overrides Then the document displays all three timestamps with the configured labels, date/time formats, and timezone abbreviations And the per‑document editor toggles reflect these defaults Given the admin changes the workspace defaults (e.g., hides Provider time, switches 24‑hour to 12‑hour) When a subsequent document is generated Then it renders using the updated defaults
Client‑Level Overrides Take Precedence Over Workspace Defaults
Given workspace defaults display Provider, Client, and UTC with workspace formats And a client profile override hides Provider time and sets client date format to dd/MM/yyyy When a document is generated for that client Then the document displays Client and UTC only And the client timestamp uses dd/MM/yyyy with the client’s timezone abbreviation And the per‑document editor initial state matches the client override Given a different client has no overrides When a document is generated for that client Then the workspace defaults apply
Per‑Document Toggles and Live Preview
Given a user is editing an invoice in the document editor When the user toggles off UTC Then the live preview removes UTC within 500 ms And the change is persisted on save and export for that document unless compliance‑required Given the user edits the label for Client time When the user saves the document Then the updated label appears identically in email, client portal, and PDF outputs Given the user clicks Reset to Defaults When a client‑level override exists Then the editor reverts to the client override Else it reverts to the workspace defaults
Consistent Rendering Across Email, Client Portal, and PDF
Given a document is saved with specific timestamp elements, labels, order, and formats When the document is sent via email, viewed in the client portal, and downloaded as PDF Then all three channels render the same elements, label text, order, date/time formats, timezone abbreviations, and UTC reference Given the document settings are modified and re‑sent When the recipient opens the new email, portal view, or PDF Then the rendering reflects the latest saved settings consistently across all channels
Compliance Guardrail: UTC Cannot Be Disabled
Given a document is marked for a compliance‑required export When a user opens the per‑document editor Then the UTC toggle is locked on and a visible notice explains that UTC is required for compliance Given a user disables UTC in workspace or client settings When a compliance‑required export is generated Then UTC remains enabled on the export regardless of those settings And the export workflow surfaces a notice that UTC was enforced due to compliance Given a non‑compliance document is exported When the UTC toggle is off Then UTC is not shown
Localization: Date/Time Formats, Timezones, and DST Accuracy
Given a client locale with date format dd/MM/yyyy and timezone Europe/London on a session at a DST boundary And the workspace default time format is 24‑hour for Provider in America/Los_Angeles When the document renders timestamps Then the Client time displays in dd/MM/yyyy with correct local time and DST‑aware timezone abbreviation (e.g., GMT/BST) And the Provider time displays in the workspace format with correct conversion and abbreviation (e.g., PST/PDT) And UTC displays in 24‑hour with label "UTC" and offset (e.g., UTC+00:00) Given the same document is viewed after a DST change date When it is re‑rendered in any channel Then timestamps remain anchored to the original session instant and do not shift with the viewer’s current DST status

Rollcall Board

A live, in-session roster that lets you mark Present, Late, or No‑Show with one tap—timestamped and synced across co‑facilitators. Status changes trigger the right downstream actions (e.g., send paylink, forfeit deposit, release seat), eliminating clipboard chaos and keeping records audit‑ready.

Requirements

One-Tap Attendance Marking
"As a facilitator, I want to mark attendees as Present, Late, or No‑Show with one tap during a session so that I can keep accurate, timestamped records without breaking my flow."
Description

Provide an in-session roster UI with large, mobile-friendly controls to set each attendee’s status to Present, Late, or No‑Show in a single tap. Each status change records an exact timestamp and the acting user, supports optional reason tags, and offers undo within a short window. Enable batch updates for multi-select attendees, keyboard shortcuts on desktop, and color/label indicators that are accessible and color-blind safe. Persist changes to a canonical Attendance model scoped to the session and participant, emitting domain events to power downstream automations. Support configurable lateness thresholds per session type and enforce a single active status per attendee at any time.

Acceptance Criteria
One-Tap Status Marking on Mobile Roster
- Given the in-session roster is open on a touchscreen device, when a facilitator taps Present, Late, or No‑Show on an attendee row, then the attendee’s status updates with a visible state change within 300 ms and a single tap is sufficient. - And the tap targets for Present, Late, and No‑Show are at least 44x44 pt with 8 pt minimum spacing. - And the previous selection is deselected so only one status is active at a time; tapping the already-active status results in no new change. - And a transient confirmation appears indicating the new status. - And optional reason tags can be added without blocking the status change; if provided, they are saved with the change.
Audit-Persisted Status Change Metadata
- Given any status change, then the Attendance record persists the change to a canonical model scoped by sessionId and participantId, capturing: oldStatus, newStatus, actedByUserId, occurredAt (ISO 8601 with timezone), reasonTag (nullable), and version. - And an immutable audit trail entry is appended with the same data and is visible in the session’s activity history. - And occurredAt uses server time; client clock skew does not affect stored timestamps.
Undo Within 10 Seconds
- Given a status change just occurred, when the facilitator taps Undo within 10 seconds, then the attendee’s previous status is restored, the audit trail records a revert entry, and an AttendanceStatusReverted event is emitted. - If 10 seconds elapse or a conflicting update occurs before Undo, then the Undo control is disabled and a message explains why. - Undo reverts only the most recent change per attendee; for batch actions, Undo reverts the entire batch as a single unit within the same window.
Domain Events, Real-Time Sync, Single Active Status, and Concurrency Control
- On every successful status change, the system emits an AttendanceStatusChanged domain event with fields: eventId, sessionId, participantId, oldStatus, newStatus, reasonTag, actedByUserId, occurredAt, and version. - Co-facilitators viewing the same roster see the updated status within 2 seconds; updates are applied in event order. - The model enforces a single active status per attendee per session; setting a new status deactivates the prior one atomically. - Concurrent updates use optimistic concurrency: if the submitted version is stale, the API returns 409 Conflict and the UI prompts to refresh; no partial updates are applied.
Batch Updates and Desktop Keyboard Shortcuts
- Facilitators can multi-select attendees (shift-click on web, long-press plus taps on mobile) and apply Present, Late, or No‑Show in one action; all selected attendees update atomically or the operation is rolled back with an error message. - Desktop keyboard shortcuts are available when a roster row is focused: P sets Present, L sets Late, N sets No‑Show; Arrow Up/Down moves focus; Enter opens reason tags. - Shortcut actions perform identically to taps, including audit metadata capture, undo window, and event emission.
Accessible, Color‑Blind Safe Status Indicators
- Status controls include both color and text labels; no information is conveyed by color alone. - Contrast ratios meet WCAG 2.2 AA: text >= 4.5:1 and non-text UI components >= 3:1. - Screen readers announce status changes and controls have descriptive ARIA labels; focus order is logical and a visible focus indicator is present. - Indicators pass simulations for protanopia, deuteranopia, and tritanopia.
Configurable Lateness Thresholds per Session Type
- Each session type stores a lateness threshold in whole minutes (>= 0) that is retrievable via API and editable by users with Manage Sessions permission. - When the roster clock exceeds the threshold and an attendee is first marked, the UI defaults selection to Late and displays the threshold; facilitators can override to Present explicitly. - When Late is set, minutesLate is computed as max(0, occurredAt - sessionStart - threshold) using server time and the session’s timezone and is included in the audit entry.
Real-Time Co-Facilitator Sync
"As a co‑facilitator, I want attendance updates to sync instantly across our devices so that we all act on the same information during the session."
Description

Synchronize attendance changes across all authorized co‑facilitators and devices in near real-time using WebSockets (with graceful fallback to polling). Show presence indicators for collaborators and reflect updates within sub-second latency under normal load. Implement conflict resolution (last-writer-wins with full audit capture) and optimistic UI updates with server reconciliation. Restrict access via session-scoped permissions and tenant isolation, and protect events with TLS and auth tokens. Provide resilience through reconnect/backoff, message ordering, and idempotent event handling.

Acceptance Criteria
Sub-Second Real-Time Propagation via WebSockets
Given two authorized co-facilitators are viewing the same session’s Rollcall Board over WebSockets When Facilitator A changes Attendee X from Scheduled to Present Then Facilitator B’s UI reflects the update within 500 ms median and ≤800 ms at p95 under normal load (≤100 concurrent connections per tenant) And Facilitator A’s other active devices reflect the update within the same latency target And the initiating client receives a server acknowledgement within 1 s including server_timestamp, actor_id, previous_value, new_value
Presence Indicators for Collaborators
Given multiple authorized co-facilitators are connected to the session When a co-facilitator connects or disconnects Then presence indicators update across all clients within 800 ms at p95 And Online shows name and device count; Offline shows last_seen within ±1 s accuracy And a disconnected client is marked Offline after 10 s of missed heartbeats and removed from Online list
Conflict Resolution and Full Audit Capture
Given two co-facilitators issue conflicting updates to the same attendee within a 2 s window When the server receives both events Then last-writer-wins is applied using server-received timestamp And both events are written to the audit log with event_id, actor_id, previous_value, new_value, received_at, resolved_by_event_id And all clients converge to the resolved state within 1.5 s and display the final value with no duplicate transitions
Optimistic UI with Server Reconciliation
Given a facilitator changes an attendee’s status Then the local UI updates immediately (<50 ms) with a syncing indicator When the server accepts the change Then the syncing indicator clears within 50 ms and the server timestamp replaces any provisional timestamp When the server rejects or overwrites the change Then the UI reverts to the prior server state within 200 ms and displays an error message with reason_code
Resilient Reconnect, Ordering, and Idempotency
Given a temporary network loss up to 30 s occurs When connectivity resumes Then the client reconnects with exponential backoff and jitter (initial 1 s, max 30 s) and resumes from the last acknowledged sequence number And missed events are applied in order with no duplicates or gaps And all clients reach a consistent state within 3 s after reconnection Given a duplicate event_id is received Then it is ignored without side effects (idempotent handling) Given out-of-order delivery is detected Then the client buffers or fetches a snapshot to preserve monotonic ordering
Secure Transport, Auth, and Session-Scoped Access Control
Given a client attempts to connect over ws:// or TLS < 1.2 Then the connection is refused; only wss:// TLS 1.2+ is accepted Given a request carries an invalid, expired, or wrong-tenant JWT Then the operation is rejected, the client is disconnected within 1 s, and no event is broadcast or stored Given a user lacks session-scoped co_facilitator:write permission Then attempts to change attendance return 403/permission_denied and are not persisted or broadcast And cross-tenant access to sessions is blocked; presence and updates are never leaked across tenants And all security-related denials are logged with reason_code, actor_id, tenant_id, and session_id
Graceful Fallback to Polling
Given WebSocket connection attempts fail or drop repeatedly (≥3 failures within 60 s) When fallback is engaged Then the client switches to polling the session state every 2 s with If-None-Match/ETag support And updates made by any facilitator are reflected across clients within 2.5 s p95 while in fallback When WebSocket becomes available again Then the client upgrades back to WebSocket within 5 s without losing state or duplicating events
Status Automation Rules
"As an account owner, I want to define what happens when someone is late or no‑shows so that billing, notifications, and seat management happen automatically without manual follow‑up."
Description

Allow admins to configure rules that map attendance states and thresholds to downstream actions, including sending payment links, applying late/no‑show fees, forfeiting deposits, releasing seats, and notifying participants. Provide a visual rule builder with conditionals (e.g., Late > 10 minutes), time windows, and per‑service overrides. Execute actions via a reliable, idempotent job queue with retries, backoff, and error logging. Expose a test/simulation mode to preview outcomes before enabling, and surface per‑action status and failures in an operations view.

Acceptance Criteria
Late > 10 Minutes Triggers Payment Link Within Window
Given an admin creates an enabled rule: IF AttendanceStatus = Late AND LateDuration >= 10 minutes AND Now within [session start, session start + 60 minutes] for Service = "Coaching", THEN send Payment Link using template "Standard Paylink" When a participant in a "Coaching" session is marked Late at 12 minutes after session start Then a Payment Link job is enqueued within 5 seconds, processed within 60 seconds, and the participant receives exactly one payment link And the Operations View shows one action with status transitions Queued -> Success, with timestamps And when LateDuration = 9 minutes, no action is enqueued And when the rule is toggled Off, no action is enqueued
Per‑Service Override Takes Precedence Over Global Rule
Given a Global rule: IF AttendanceStatus = No‑Show THEN Forfeit Deposit (enabled) And a Service override for Service = "Workshop": IF AttendanceStatus = No‑Show THEN Send Payment Link (enabled) When a participant in a "Workshop" session is marked No‑Show Then only the Send Payment Link action is executed and logged; no Forfeit Deposit job is enqueued And the Operations View records the applied rule id and indicates "Override applied" And when a participant in a different service without override is marked No‑Show, the Global Forfeit Deposit action executes And if both rules could match, the system resolves by priority and executes exactly one action; the losing rule is logged as "Superseded"
Idempotent Execution with Retries and Exponential Backoff
Given each action job uses a deterministic deduplication key {rule_id, session_id, participant_id, action_type} with a 24‑hour window When the same status change event is received multiple times within 24 hours Then at most one outbound action is executed; subsequent duplicates are skipped and logged as "Duplicate - Skipped" And when the downstream endpoint returns HTTP 5xx, the job retries up to 3 times with exponential backoff of 1m, 2m, 4m And upon final failure, the action status is "Failed" with error code, message, and last attempt timestamp visible in Operations View And if a retry later succeeds, there is exactly one external effect and the final status is "Success"
Simulation Mode Preview Without Side Effects
Given a rule in Simulation Mode (not enabled) and a test cohort of the last 50 sessions When the admin clicks "Run Simulation" with parameters LateDuration = 15 minutes Then the system renders a preview within 2 seconds listing predicted actions, recipients, amounts, and scheduled times And no messages are sent, no fees applied, no deposits forfeited, and no seats released; no jobs are enqueued And the preview items are labeled "Simulated" and can be exported to CSV; exported counts match on‑screen totals
Time Windows and Timezone‑Aware Evaluation
Given a rule with a trigger window: only between T0+10m and T0+30m relative to session start (T0) using the session's timezone When a session starts at 09:00 America/New_York and a participant is marked Late at 09:25 local time Then the rule triggers and enqueues exactly one action And when marked at 09:35 local time, the rule does not trigger And the rule builder displays times in the admin's local timezone, stores in UTC, and evaluation uses the session's timezone; behavior remains correct across a DST change And the audit log records the evaluated window and timezone used
Operations View Shows Per‑Action Status, Filtering, and Export
Given actions are being executed by the job queue When viewing the Operations View for a selected date range Then each action row displays Job ID, Rule Name, Service, Participant, Action Type, Status (Queued, Running, Success, Failed), Attempt Count, CreatedAt, UpdatedAt And filters by date range, rule, service, action type, status, and text search by participant email update results within 500 ms for up to 10,000 records And selecting a row opens details with request/response payloads (sensitive fields masked), error stack (if any), and deduplication key And exporting CSV returns only filtered rows and the total count matches the on‑screen results
Seat Release and Waitlist Notification Are Atomic and Single‑Fire
Given a rule: IF AttendanceStatus = No‑Show THEN Release Seat and notify next waitlisted contact And two co‑facilitators mark the same participant as No‑Show within 2 seconds When the system processes the resulting events Then exactly one seat release occurs and one waitlist notification is sent to the next eligible contact within 60 seconds And seat inventory updates are atomic; subsequent duplicate attempts are skipped with a "Duplicate - Skipped" log entry And if no waitlist exists, the action completes with status "Success - No Waitlist" and no notification is sent
Audit Trail & Export
"As a practice owner, I want an audit-ready record of attendance changes so that I can resolve disputes and meet compliance expectations during reviews."
Description

Capture an immutable, chronological log of every attendance-related event, including who changed what, before/after values, timestamps with timezone, device/IP metadata, and related automation actions. Store logs with append-only semantics and safeguard against tampering. Provide filtering by session, participant, date range, and user, plus CSV/PDF export for audits and dispute resolution. Enforce role-based access and data minimization, and align exports with organizational retention settings.

Acceptance Criteria
Append-Only Audit Log on Attendance Update
Given a facilitator changes a participant’s status from Late to Present in a live session When the change is saved Then a new audit entry is appended with action=attendance_update, before=Late, after=Present, actor_user_id set, session_id and participant_id set, event_time recorded in ISO 8601 with timezone offset, device_type and ip_address captured And the entry contains an immutable event_id and prev_hash linking to the previous entry in the session log And any attempt to update or delete existing audit entries via UI or API is rejected (HTTP 403/405) and no database row is altered And fetching the session audit history returns all prior entries unchanged and ordered strictly by event_time then event_id
Automation Actions Correlated in Audit Trail
Given a status change triggers an automation (e.g., send paylink, forfeit deposit, release seat) When the automation is queued and executed Then the audit log records a distinct automation event with automation_type, automation_status (queued|sent|failed), target reference, and correlation_id linking to the originating status-change event And retries generate additional automation events with attempt number and final outcome captured And the originating status-change event includes automation_triggered=true with the same correlation_id
Comprehensive Event Detail Capture
Given any attendance-related action (mark Present/Late/No‑Show, undo, edit note, bulk update) When the action is saved Then the audit entry includes before_value, after_value, actor_user_id, actor_role, event_time with timezone offset, source (UI|API|automation), device_type, ip_address, and optional reason/comment if provided And bulk actions create one entry per participant with a shared bulk_operation_id And all timestamps are stored in UTC and rendered with the organization’s timezone offset in views/exports
Granular Filtering of Audit Log
Given audit entries exist across multiple sessions and users When the user filters by session_id, participant_id, date range (start/end), and actor_user_id in any combination Then results include only entries matching all selected filters (logical AND) And date boundaries are interpreted in the organization’s timezone and converted correctly from/to UTC And the first page of 100 results returns in ≤2 seconds at P95 for datasets up to 50k entries And results are ordered by event_time then event_id and support stable cursor-based pagination (next/prev)
CSV and PDF Export with Integrity and Retention
Given a user with export permission applies any valid filter set When they export to CSV or PDF Then the file contains exactly the filtered results and includes a header block with org_id, filters_applied, generated_at (ISO 8601 with timezone), record_count, and checksum And CSV escapes commas, quotes, and newlines; uses UTF-8; preserves leading zeros; and includes timezone offsets in timestamp columns And PDF paginates with repeating header/footer, page numbers, and preserves selectable text And a SHA-256 checksum is displayed post-generation and embedded in the file header/metadata; regenerating the same export with identical filters within 15 minutes yields the same checksum And retention rules exclude records outside the organization’s retention window; the export and UI display an explicit notice of exclusions with counts
Role-Based Access and Data Minimization
Given multiple user roles (Owner/Admin, Facilitator, Biller, Auditor, External) When accessing or exporting audit logs Then only Owner/Admin/Auditor can view full audit metadata and export all fields And Facilitators may view logs only for sessions they facilitate; ip_address is masked to /24 and device_type generalized (e.g., "mobile"/"desktop"); PII fields (email/phone) are partially masked And Biller can view/export only financial-automation events with participant identifiers minimized (e.g., participant_id but no contact details) And External users receive HTTP 403 with no data leakage in response headers/body And all access attempts (allowed/denied) are themselves logged with actor_user_id, purpose=access_audit, and event_time
Concurrent Updates and Ordering Consistency
Given two co‑facilitators update the same participant’s status within 3 seconds from different devices/IPs When the system processes both changes Then two distinct audit entries are recorded with unique event_id, precise event_time, device_type, and ip_address for each action And the final participant status reflects last-write order determined by server-side event_time then event_id; both entries remain in the log And all connected clients receive the ordered sequence within 1 second of commit; filtered views and exports reflect the same order
Billing & Paylink Generation
"As a solo practitioner, I want attendance to automatically drive invoice creation and paylink delivery so that I get paid promptly without manual data entry."
Description

Integrate attendance outcomes with SoloPilot invoicing to automatically create or update invoices and line items (session fees, late fees, no‑show charges), apply deposit forfeitures or credits, and generate a secure payment link. Send paylinks via the client’s preferred channel (email/SMS) using templates, and prevent duplicate billing through idempotent invoice reconciliation. Support tax rules, multi-currency accounts, and syncing payment status back to the roster in real time.

Acceptance Criteria
Auto‑Invoice Creation on Present Status
Given a session is marked Present for a client with a defined session fee and tax profile When the Present status is saved on the Rollcall Board Then an invoice is created or updated for that session in the client’s currency within 5 seconds And a single session fee line item is added with the session date as the service date And taxes are calculated per the client’s tax rules and jurisdiction And the invoice is set to Pending (or updated if already exists) and linked to the session ID for reconciliation And the operation is logged with a unique idempotency key per client-session
Late Status Adds Late Fee and Generates Paylink
Given a session is marked Late and a late-fee policy is enabled for the client or service When the Late status is saved Then the system adds exactly one late fee line item according to the configured policy and tax rules (no duplicates on retries) And updates or creates the invoice linked to the session And generates a secure paylink for the invoice total and returns the URL within 5 seconds
No‑Show Triggers Deposit Forfeiture or No‑Show Charge
Given a session is marked No‑Show and the client has a deposit on file When the No‑Show status is saved Then the deposit is forfeited by applying it to the session’s invoice according to policy and tax rules And if the deposit fully covers the charge, the invoice is marked Paid and no paylink is sent And if a balance remains, a paylink is generated for the remaining amount And if no deposit exists, a no‑show line item is added and a paylink is generated And only one forfeiture or no‑show charge is applied per session (idempotent)
Idempotent Invoice Reconciliation and Duplicate Prevention
Given multiple identical events (retries, concurrent taps, or webhooks) for the same client-session When invoice creation or update is requested Then the system uses an idempotency key derived from client ID + session ID + outcome to ensure at most one invoice is created And existing invoices are updated rather than duplicated And session fee, late fee, and no‑show line items appear at most once per session And reconciliation links the invoice to the session and roster entry for audit
Secure Paylink Generation and Preferred Channel Delivery
Given an invoice with an outstanding balance and a client with a preferred contact channel (Email or SMS) When a paylink is generated Then the link is HTTPS, single‑use tokenized, and expires per policy (default 14 days) And opening the link displays itemized charges, taxes, currency, and total clearly And the paylink is sent via the client’s preferred channel using the selected template with variables resolved (name, amount, due date) And if the preferred channel is unavailable or opted‑out, the system falls back to the other channel and records the fallback And delivery status (queued/sent/failed) is recorded; failures are retried up to 3 times with exponential backoff
Real‑Time Payment Status Sync to Rollcall Board
Given a payment is completed or partially paid via the paylink or external gateway When the payment provider webhook is received Then the invoice status and paid amount are updated within 5 seconds And the Rollcall Board reflects Paid/Partial with the updated balance and timestamp And a receipt is sent to the client via their preferred channel And duplicate webhooks do not change the final state beyond idempotent reconciliation
Tax Rules and Multi‑Currency Compliance
Given the account supports multiple currencies and jurisdiction‑specific taxes When invoices and line items are created for a session outcome Then amounts are denominated in the client’s currency with correct symbol and formatting And tax is computed per configured rules (e.g., VAT/GST/sales tax, tax‑inclusive vs tax‑exclusive) with rounding to currency precision And exchange rates are sourced from the configured provider with the rate and timestamp recorded on the invoice And tax breakdowns (rate, jurisdiction, amount) are stored for audit and displayed on the paylink And totals reconcile exactly: sum(line items + tax) == invoice total within currency rounding rules
Waitlist & Capacity Release
"As a facilitator, I want seats freed by no‑shows to auto-offer to my waitlist so that I maximize attendance and revenue without manual coordination."
Description

Manage session capacity and integrate with waitlists so that a No‑Show or cancellation can release a seat and automatically offer it to the next eligible client. Configure hold windows, acceptance flows, and notification templates. Update the roster upon acceptance, handle declines or timeouts, and adjust deposits and fees according to policy. Prevent overbooking via atomic seat allocation and provide an activity feed that shows offer, accept, and expire events.

Acceptance Criteria
Seat Release on No-Show or Cancellation Triggers Waitlist Offer
Given a session is at capacity and has a non-empty waitlist And policy states a No-Show or Cancellation releases a seat When a rostered attendee is marked No-Show in Rollcall or cancels and the status is saved Then exactly one seat becomes available And the next eligible waitlisted client is selected per priority rules And an offer is created with a configured hold window and sent via selected channels And deposits/fees for the original attendee are adjusted per policy (e.g., forfeit deposit) And an "offer" activity event is logged with client, seat, policy, and timestamp
Offer Hold Window, Expiration, and Timezone Consistency
Given an offer with a hold window of W minutes and session timezone TZ When the offer is issued at t0 Then the seat is reserved for the offered client until t0+W in TZ and unavailable to others And the offer displays the exact expiration in both the client's local time and TZ When the hold window elapses without acceptance Then the offer status becomes Expired, no payment is captured, and the seat is released or re-offered per configuration And an "expire" activity event is logged and the next client is queued if configured
Atomic Seat Allocation Under Concurrent Acceptances
Given multiple clients may attempt to accept an offer for the same seat concurrently When two or more acceptance transactions are submitted in overlapping time Then only the first committed transaction allocates the seat and creates the booking And subsequent transactions are rejected with a clear "seat no longer available" message and zero payment capture And roster capacity never exceeds the configured limit And a single "accept" event is logged for the successful allocation; rejected attempts are logged as "conflict" without allocation
Acceptance Flow Updates Roster, Billing, and Policy Adjustments
Given an active offer requiring deposit D or full payment P When the client accepts within the hold window and completes any required payment Then the system captures D or P, generates an invoice/receipt, and records payment reference And the client is added to the roster with status Confirmed and seat count is decremented And notifications are sent to the client and all facilitators And the waitlist entry is marked Fulfilled And an "accept" activity event is logged with payment reference and applied policy And the Rollcall Board reflects the new attendee across co-facilitators within 2 seconds
Decline or Timeout Auto-Advances and Prevents Repeat Offers
Given an active offer is outstanding to a waitlisted client When the client clicks Decline or the hold window expires Then the offer is immediately closed and cannot be accepted thereafter And the next eligible waitlisted client receives a new offer with a fresh hold window And the declining or expired client is not re-offered the same seat unless manually re-queued And corresponding "decline" or "expire" events are logged with timestamps and actor
Notification Templates and Activity Feed for Offer Lifecycle
Given message templates with variables {client_name}, {session_time}, {hold_expires_at}, {paylink} When an offer is sent, accepted, declined, or expired Then the system sends the appropriate message via configured channels (email, SMS, in-app) within 30 seconds And all variables render correctly with no missing placeholders And the activity feed records event type, actor/system, recipient, ISO 8601 timestamps, delivery status, and links to artifacts (invoice, message) And feed entries are immutable and exportable for audit
Eligibility Rules and Priority Ordering for Waitlist Offers
Given a waitlist with clients having varied eligibility (prerequisites, balances, consent) When selecting the next client for a seat offer Then only clients satisfying eligibility rules are considered (no outstanding balance, prerequisites complete, consent on file, not banned) And ineligible clients are skipped with the specific reason logged on their waitlist record And selection respects priority ordering (position, timestamp) with deterministic tiebreakers And if no eligible clients remain, the seat is opened to public signup per configuration and the action is logged
Offline Attendance Capture
"As a field coach, I want to record attendance when I’m offline so that my session records remain accurate and synchronize automatically once I’m back online."
Description

Enable attendance marking without network connectivity using a local-first store that queues changes for background sync. Preserve the recorded event time distinct from the sync time, display sync status per attendee, and resolve conflicts deterministically with full audit capture. Encrypt local data at rest, allow admins to enforce offline mode policies, and provide clear user feedback for partial failures with retry options.

Acceptance Criteria
Offline Mark and Queue with Event Timestamp Preservation
Given the device is offline and a facilitator opens a live Rollcall Board for Session S When they tap Present for Attendee A at 10:02 local time Then the app saves a local record with status=Present, eventTimestamp=10:02, syncState=Pending, and no network call is attempted And Given multiple marks are made offline for attendees A, B, and C When the device remains offline Then all records are persisted to the encrypted local store and visible in the roster with a Pending badge And When connectivity is restored Then the app initiates background sync within 10 seconds and does not alter eventTimestamp values
Per-Attendee Sync Status and Partial Failures
Given attendees A, B, and C have Pending, Synced, and Failed sync states after an attempt When viewing the roster Then A shows a Pending badge, B shows a Synced checkmark, and C shows an Error icon with a message including an error code And When the user taps Retry All or taps Retry on attendee C Then only failed or selected items are retried with exponential backoff and updated statuses are reflected in the UI And If a retry succeeds Then C’s badge changes to Synced without duplicating the server record
Deterministic Conflict Resolution with Full Audit
Given attendee A has conflicting records: Present@10:02 from device D1 (offline) and No-Show@10:04 from device D2 (online) When sync occurs Then the system resolves to No-Show using the later eventTimestamp 10:04 And Given a tie on eventTimestamp across conflicting records When resolving Then the system applies deterministic tiebreakers in order: facilitatorRole priority (Owner > Facilitator > Assistant), then higher clientLogicalClock, then lexicographically smaller deviceId And Then the audit log stores both submitted records, the resolved outcome, the applied tiebreaker, resolver=system, and timestamps, and is queryable by session and attendee
Local Data Encryption and Lifecycle Controls
Given the app stores offline attendance data When inspecting device storage at rest Then data is encrypted using OS keystore-backed AES-256-GCM and is unreadable without app context And When the user logs out or an admin policy triggers purge Then all offline attendance records and local encryption keys are securely wiped from the device within 5 seconds And When the device is locked Then offline data remains inaccessible until the device is unlocked And When the app is uninstalled Then no offline attendance data remains on the device
Admin-Configurable Offline Policy Enforcement
Given workspace setting offlineCapture=Disabled When a facilitator is offline Then the Rollcall Board disables status changes and shows a banner stating Offline attendance not allowed by admin And Given workspace setting offlineCapture=Enabled with maxOfflineAge=72h and requireDeviceLock=true When a device lacks a screen lock or a record exceeds 72h pending Then the app blocks new offline marks and flags expired records with Expired — not synced And When policy is updated server-side Then devices fetch and apply the new policy within 15 minutes or on next app foreground
Queued Downstream Actions with Idempotency
Given marking No-Show triggers a deposit forfeit and email When the mark is recorded offline Then the app creates queued downstream actions with stable idempotency keys linked to the attendance record And When sync completes Then each action executes exactly once server-side regardless of duplicate retries, and time-sensitive logic uses the original eventTimestamp And Then the system logs action execution status per attendee and surfaces failures with per-action Retry options
Clear Offline UX and Accessibility
Given the device goes offline during a session When the user opens Rollcall Board Then an offline banner appears within 2 seconds, the last synced time is shown, and the offline-capable UI remains responsive And When the user attempts a restricted action while offline Then the app explains the restriction and suggests available offline alternatives And All offline and sync status indicators have text labels, meet WCAG 2.1 AA contrast, and are announced by screen readers

QR Check‑In

Each attendee receives a unique QR code in their reminder. Scanning it at the door self‑marks attendance, collects any missing details/consents, and instantly launches their paylink. Kiosk mode and offline tolerance keep lines moving, reducing pre‑work and on‑site admin.

Requirements

QR Generation & Reminder Delivery
"As an attendee, I want to receive a secure QR code tied to my booking so that I can check in quickly without spelling my name at the desk."
Description

Generate a unique, signed, time-bound QR token per attendee per session that encodes booking/client references and a nonce. Tokens must be tamper-resistant (e.g., HMAC/JWT), support single-use with idempotency, configurable expiry windows, and optional grace periods for late arrivals. Embed the QR in email/SMS reminders with templating and localization support, plus a fallback web link and re-send/regenerate options. Deep-link scans to a secure check-in endpoint that validates the token, enforces rate limiting, and logs audit trails. Integrates with SoloPilot’s scheduling and messaging modules to ensure the QR mirrors the latest booking state (reschedules, cancellations).

Acceptance Criteria
Unique Signed Token Generation with Nonce and Booking References
Given a confirmed booking with attendee_id A and session_id S, when a reminder is generated, then a token T is created that embeds booking_id, attendee_id, session_id, a cryptographically secure random nonce (>=128 bits), iat, and exp. And T is signed using HMAC-SHA256 or RS256 with a server-held secret/private key, and base64url-encoded as a compact string (e.g., JWT). When any byte of T is altered, then signature verification fails and the token is rejected with error_code=invalid_signature. When reminders are generated for two different attendees for the same session, then the resulting tokens are distinct. When a reminder is regenerated for the same attendee-session, then the new token contains a different nonce and token_id from the prior token. Then the QR encodes only the opaque token T (no plaintext PII), and the generated QR is scannable at error correction level M or higher.
Single-Use Idempotent Check-In
Given a valid, unused token T, when the check-in endpoint is called with T, then an attendance record is created exactly once and the response returns 200 with check_in_id and token_id. Given the same token T is submitted again, when the check-in endpoint is called, then the response returns 200 with the same check_in_id and already_checked_in=true, and no additional attendance or side effects are produced. Given two concurrent requests arrive with the same token T, when processed, then only one attendance record exists and both responses reflect a single canonical check_in_id. Given a token T that has been marked used, when any further requests with T are made, then the outcome remains idempotent and no duplicate records or duplicate downstream actions are triggered.
Configurable Expiry Window and Grace Period Enforcement
Given the token window is configured to open 24 hours before session_start and close 10 minutes after session_start, when a token T is generated, then T.exp reflects session_start+10m and validation rejects T before window open and after window close by default. Given the optional grace period is set to 15 minutes, when T is presented up to 15 minutes after T.exp, then the check-in is accepted and outcome reason indicates grace_applied=true. Given grace period is disabled, when T is presented after T.exp, then validation fails with 401 and error_code=token_expired. Given T is presented before the configured window opens, when validation occurs, then the request is rejected with 403 and error_code=window_not_open.
Reminder Delivery: Embedded QR, Fallback Link, Templates, and Localization
Given an email reminder is sent, when it is generated, then it includes an embedded QR image for token T, accessible alt text, and a fallback deep-link URL to the check-in page. Given an SMS reminder is sent, when it is generated, then it includes a shortened fallback deep-link URL to the check-in page and renders within 160 characters for English default template. Given recipient language is set to es-ES and templates have translations, when reminders are generated, then subject/body and date/time formats are localized (e.g., 24h time, localized month/day names); when a translation key is missing, the system falls back to the workspace default language without template breakage. Given template placeholders for {{attendee_first_name}}, {{session_start_time}}, and {{location_name}}, when reminders are rendered, then placeholders are replaced with correct values from the latest booking state. Given delivery is attempted, when the provider returns success or failure, then the system records delivery status and timestamp per channel for audit and troubleshooting.
Resend and Regenerate Handling with Prior Token Invalidation
Given a staff user triggers a reminder resend for attendee A in session S, when the resend action is confirmed, then a new token T2 is generated and all prior tokens T1..Tn for A+S are invalidated. Given an invalidated token T1 is scanned, when validation occurs, then the request is rejected with 401 and error_code=token_invalidated. Given the resend action is triggered multiple times within 60 seconds, when processing occurs, then the action is idempotent and at most one new token is active, with duplicate UI clicks not producing additional active tokens. Given resend is requested for both email and SMS channels, when processed, then both channels receive reminders containing the new token T2 within the messaging SLA, and message logs reflect channel-specific outcomes.
Booking State Sync on Reschedules and Cancellations
Given a booking is rescheduled from session S to session S', when the reschedule is saved, then all tokens for S are invalidated and a new token for S' is generated; subsequent scans of old tokens return 409 with error_code=booking_rescheduled and include a link to request a fresh reminder. Given a booking is cancelled, when a previously issued token is scanned, then validation returns 410 Gone with error_code=booking_cancelled and no attendance record is created. Given a booking is updated (e.g., attendee name or location changes), when new reminders are generated, then template variables reflect the latest values and previously sent tokens remain valid only if the session/time remains unchanged.
Secure Check-In Endpoint: Validation, Rate Limiting, and Audit Trail
Given a request to the check-in endpoint over HTTP, when processed, then it is redirected or rejected; only HTTPS requests are accepted for token validation. Given a token T is presented, when validation runs, then the system verifies signature, checks exp/window and grace settings, ensures attendee_id and session_id map to an active booking, and confirms the token has not been invalidated or used in violation of single-use rules. Given more than 10 requests from the same IP are received within 60 seconds, when additional requests arrive, then the endpoint returns 429 Too Many Requests with a Retry-After header; similarly, more than 5 requests per minute for the same token_id yield 429. Given any validation attempt occurs (success or failure), when logging executes, then an audit event is recorded with timestamp, token_id, booking_id, attendee_id, session_id, IP, user_agent, outcome (success/failure), reason_code, and correlation_id; logs are immutable and queryable by booking_id and time range.
Kiosk Mode Check-In
"As a solo practitioner, I want a kiosk mode that scans codes rapidly and securely so that lines move fast with minimal supervision."
Description

Provide a full-screen, lockable web kiosk optimized for rapid scanning with device camera or USB/Bluetooth scanners. Auto-focus and continuous scan mode with clear visual/audio feedback for success/failure, large accessible UI, and branded theming per workspace/location. Privacy protections (mask PII, obfuscate DOB/phone), no browser chrome, and PIN-protected exit. Support multi-session queues, on-screen instructions, and fallback manual lookup by code or name if necessary. Compatible with tablets and desktops; responsive and WCAG AA compliant. Logs actions locally for resilience and syncs to the server when connected.

Acceptance Criteria
Full-Screen Lockable Kiosk & Secure Exit
Given an operator enables Kiosk Mode, When the kiosk loads, Then the application requests browser fullscreen and renders without in-app navigation, external links, or visible scrollbars. Given Kiosk Mode is active, When a user attempts to exit, Then a 6-digit PIN prompt is required and exit occurs only on correct PIN. Given an incorrect PIN is entered 5 times within 2 minutes, When further attempts are made, Then the kiosk enforces a 60-second lockout and logs the attempts locally. Given 30 seconds of no interaction on the check-in screen, When the timeout elapses, Then the kiosk auto-resets to the Ready-to-Scan state. Given the page reloads or the device reboots, When the kiosk relaunches, Then Kiosk Mode resumes automatically with the same location/workspace configuration. Given Kiosk Mode is active, When a user right-clicks or long-presses, Then the context menu is suppressed.
QR Scan Input Modes (Camera and Peripheral)
Given camera permissions are granted, When a valid SoloPilot QR code is centered within the scan frame at 10–50 cm, Then the code is decoded within 1 second with auto-focus and exposure adjustment. Given a USB/Bluetooth scanner is connected and configured as a keyboard wedge, When it transmits a valid SoloPilot check-in payload ending with Enter or Tab, Then the kiosk processes the payload within 150 ms. Given an invalid or expired QR/payload is scanned, When decoding completes, Then the kiosk shows a red failure state with error text and an audible failure tone and returns to Ready-to-Scan within 1 second. Given a valid QR is scanned, When check-in is processed, Then the kiosk emits an audible success tone and displays a green confirmation with attendee initials for 2 seconds before returning to Ready-to-Scan. Given camera access is denied, When the kiosk loads, Then the UI defaults to peripheral scanner mode and displays clear on-screen instructions to use a scanner or manual lookup.
Continuous Scan Mode with Feedback and Duplicate Prevention
Given Continuous Scan is enabled, When a successful scan occurs, Then the kiosk automatically returns to Ready-to-Scan within 500 ms without requiring a tap. Given the same attendee is scanned again within 30 seconds, When processing occurs, Then the kiosk displays a duplicate warning (amber) and plays a distinct duplicate tone and does not create a second attendance record. Given multiple scans occur in quick succession, When processing them, Then the kiosk sustains at least 20 successful scans per minute without UI lag on a 2019 iPad or equivalent. Given audio is enabled, When success/failure/duplicate events occur, Then the kiosk plays distinct tones at configured volume; when audio is muted, visual cues still appear with WCAG AA contrast. Given an unreadable QR is detected, When the camera feed is active, Then the UI displays a "Move closer/hold steady" hint and maintains focus attempts until either a valid read or a 5-second timeout occurs.
PII Privacy Masking on Kiosk UI
Given an attendee is recognized, When confirmation is shown, Then display only first name and last initial (e.g., "Alex J.") and never show full email. Given phone number data exists, When disambiguation or confirmation is shown, Then render as XXX-XXX-1234. Given date of birth exists, When any DOB is displayed, Then render as MMM **, **** (e.g., Jan **, ****). Given manual lookup results are listed, When multiple matches are shown, Then each row includes only masked identifiers (e.g., "Alex J., XXX-XXX-1234") sufficient for selection without revealing full PII. Given screenshots are taken, When the confirmation screen is displayed, Then no full PII is present in the UI.
Offline Check-In with Local Queue & Sync
Given the device loses connectivity, When a valid scan occurs, Then the kiosk records the check-in locally with timestamp and shows an amber "Queued (Offline)" state within 200 ms. Given offline queued events exist, When connectivity is restored, Then the kiosk syncs all queued events within 10 seconds, marking each as "Synced" and updating the UI. Given a queued event was already processed on the server, When sync occurs, Then the kiosk performs idempotent reconciliation (no duplicate attendance) and marks the local record as "Synced (Duplicate Skipped)". Given storage limits, When more than 5,000 events are queued, Then the kiosk warns the operator via an admin banner (PIN-gated) and continues queueing the most recent events while retaining at least the last 5,000. Given offline mode is active, When the kiosk is closed and reopened, Then the local queue persists and resumes syncing on reconnect.
Manual Lookup Fallback (Code or Name)
Given a user taps "Can't scan?", When the manual lookup sheet opens, Then the on-screen keyboard appears and focus is placed in the input field. Given a 6–12 character check-in code is entered, When Submit is pressed, Then validation occurs client-side and success/failure feedback is shown within 500 ms. Given a name is typed, When the user pauses for 200 ms, Then a list of up to 10 matches appears within 500 ms with masked identifiers for disambiguation. Given multiple matches exist, When a row is selected, Then the kiosk confirms selection and proceeds to check-in flow consistent with QR success. Given no matches exist, When a search is submitted, Then the kiosk offers "Try different spelling" and a "Create Walk-In (if enabled)" option when policy allows.
Multi-Session Queue Selection and Routing
Given multiple sessions are active for the location within the current time window, When a valid scan occurs, Then the kiosk auto-routes the attendee to their scheduled session; if unscheduled, it presents a session list sorted by start time with the most probable session preselected. Given a session has reached capacity, When an unscheduled attendee attempts to join it, Then the kiosk prevents assignment and displays "Capacity Reached" with alternatives. Given the attendee is scheduled for a conflicting time, When a scan occurs, Then the kiosk prompts to confirm move with a clear warning and requires operator PIN if policy requires overrides. Given offline mode, When session assignment occurs, Then the selected session is stored locally with the check-in event and reconciled on sync without loss. Given the location theme is configured, When the session list is shown, Then the list uses the location’s branding while maintaining privacy masking and readability.
Offline Check-In & Sync
"As a practitioner operating in low-connectivity venues, I want check-in to work offline and sync later so that the flow doesn’t stall without internet."
Description

Enable offline operation by pre-caching today’s roster and configuration, and queuing scan events in encrypted local storage when the network is unavailable. Provide clear offline indicators and continue validating tokens locally using cached public keys and token metadata. On reconnect, perform conflict-aware sync with deduplication (idempotency keys), server-side revalidation, and deterministic resolution for late/duplicate scans. Handle clock skew and token expiry rules gracefully. Ensure no data loss and maintain an auditable timeline of offline and post-sync events.

Acceptance Criteria
Pre-cache Today’s Roster, Config, and Keys
Given network connectivity and the operator opens QR Check-In for today’s sessions When the app enters Check-In or Kiosk mode Then it downloads and caches today’s roster, consent/config, payment templates, and JWKS/public keys, and shows “Offline Ready” within 10 seconds for up to 500 attendees Given a successful pre-cache When the device goes offline Then local cache is used for lookups and validation with zero network calls Given cached data older than today or missing JWKS When entering Check-In Then the UI shows “Not Ready for Offline” with a Retry action and does not allow Kiosk start until cache refresh succeeds or the operator explicitly proceeds with “Limited Offline” mode
Offline Scan Queue with Encrypted Local Storage
Given the device is offline When 50 unique QR codes are scanned within 5 minutes Then 50 scan events are appended to a local encrypted queue, each containing attendee/session identifiers, client scan timestamp, deviceId, and an idempotencyKey, and no PII is readable in plaintext when inspecting on-disk files Given an offline scan is captured When the operator or attendee views the confirmation screen Then feedback (success or reason) appears within 300 ms per scan and the UI remains responsive at 2+ scans/second Given the device remains offline When the app is force-closed and restarted Then the queued scan count and contents are unchanged and ready to sync on reconnect
Local Token Validation (Signature, Expiry, and Clock Skew)
Given cached JWKS/public keys and token metadata are present When a QR token is scanned offline Then the token signature is verified locally and the attendee is marked Checked-in if valid Given token expiry is within ±10 minutes of device time When scanned offline Then the app applies a 10-minute skew tolerance; within tolerance is marked Checked-in (grace), beyond tolerance is marked Rejected (expired) Given a token keyId is unknown to the cache When scanned offline Then the check-in is recorded as Pending Validation with limited access and is re-evaluated on reconnect
Kiosk Mode Offline UX and Indicators
Given Kiosk mode is active When network connectivity is lost Then an always-visible offline indicator (icon + text) appears within 1 second and Kiosk remains locked behind PIN even while offline Given a successful offline scan in Kiosk When payment cannot be launched due to no network Then attendance is marked locally and a banner states “Payment link will be sent on reconnect,” with no blocking modal Given connectivity is restored When the app detects network Then the offline indicator clears within 2 seconds and queued payment link actions are dispatched automatically
Durable Queue Under Restarts and Low Storage
Given 1% battery and the device powers off unexpectedly while offline with 100 queued scans When the device reboots and the app is reopened offline Then all 100 queued scans are present and intact Given available storage falls below 10 MB while scanning offline When additional scans occur Then the app prioritizes queue durability by pruning non-critical caches and continues to record at least 100 more scans, or otherwise blocks new scans with a clear “Storage Full—Preserving Existing Check-ins” message without losing existing data Given the queue exceeds 5,000 events When the operator views queue status Then the UI reports total pending count and oldest/newest timestamps without performance degradation (>60 FPS)
Reconnect Sync with Idempotency and Deterministic Conflict Resolution
Given the device reconnects with 1,000 queued scans When sync begins Then all 1,000 events are uploaded within 60 seconds on a 10 Mbps connection using idempotency keys, and retries do not create duplicates Given two offline scans and one online scan exist for the same attendee/session When the server processes them Then the earliest valid scan by timestamp is canonical (Checked-in) and the others are marked Duplicate referencing the canonical event Given an offline scan was accepted under expiry grace When server-side revalidation occurs Then the record is set to Rejected (expired) if outside server-time tolerance, and the UI displays the change within 2 seconds of sync completion Given network drops mid-sync When connectivity resumes Then sync resumes from the last acknowledged event without re-uploading accepted records
End-to-End Audit Trail for Offline and Post-Sync Events
Given scans occur both offline and after reconnect When an admin views the audit log for a session Then each scan appears as an immutable entry with eventId, attendeeId, deviceId, clientTime, serverTime (if synced), source (offline/online), status (e.g., Checked-in, Duplicate, Rejected), reason, and idempotencyKey, ordered chronologically Given a record is deduplicated or corrected on sync When viewing the audit trail Then a new audit entry is added that references the prior entry; the prior entry is not deleted or overwritten Given an export is requested When the admin downloads the log Then a CSV and JSON export are produced containing all fields above for independent audit within 10 seconds for up to 10,000 records
Auto Attendance & Session Link
"As a practitioner, I want scans to automatically mark attendance and link to the scheduled session so that notes and billing are prepped without manual steps."
Description

Upon successful scan, automatically mark the attendee as Present on the correct session with timestamp, device/location metadata, and operator context (kiosk vs. staff). Validate session eligibility (time window, status) and surface clear errors for canceled/expired tokens. Trigger SoloPilot automations to pre-populate session notes, create or update a draft invoice line item per service pricing rules, and set tasks for follow-up where configured. Ensure full idempotency so repeated scans do not double-mark attendance or duplicate invoices. Record an immutable attendance audit entry for compliance.

Acceptance Criteria
Successful Scan Marks Present with Metadata
Given a valid attendee QR token for a scheduled session within the configured check-in window And the scanner is in kiosk or staff mode When the QR is scanned Then mark the attendee as Present on the session encoded by the token And persist timestamp_utc in ISO 8601 (to the second) And persist device metadata (device_type, os, app_context, user_agent) And persist operator_context = "kiosk" or "staff" and operator_id when applicable And persist location metadata derived from IP (country, region, city) and GPS if permission granted And return 200 with attendance_id and session_id
Eligibility Validation and Error Messaging
Given a QR scan attempt When the session status is canceled or completed Then respond 409 with error_code = "SESSION_NOT_ELIGIBLE" and do not mark attendance When the current time is outside the configured check-in window Then respond 422 with error_code = "CHECKIN_WINDOW_CLOSED" and do not mark attendance When the token is expired, revoked, or has invalid signature Then respond 401 with error_code = "TOKEN_INVALID" and do not mark attendance And all error responses include a human-readable message and trace_id And no automations, notes, or invoice updates are performed on error
Idempotent Repeated Scans
Given the attendee is already Present for session S via this QR token When the same QR token is scanned again (including concurrent scans) Then return 200 with idempotent = true and the existing attendance_id And do not create additional attendance records And do not create duplicate or additional draft invoice line items And do not retrigger "once per attendance" automations And concurrent scans within 1 second still result in a single attendance record
Automation Triggers on Attendance
Given invoicing rules, note templates, and follow-up tasks are configured for the service When attendance is marked Present via QR scan Then create or update a draft invoice line item per the service pricing rules for the session And pre-populate session notes using the configured template and include scan metadata (timestamp_utc, operator_context) And create any configured follow-up tasks assigned per workflow settings And these side effects execute at most once per attendance_id And complete synchronously within 5 seconds or are queued with eventual completion within 2 minutes, returning operation status
Immutable Attendance Audit Trail
Given the first successful attendance mark for a session When the audit entry is written Then record an immutable, append-only audit entry with fields: attendance_id, session_id, attendee_id, previous_status, new_status, timestamp_utc, operator_context, operator_id (nullable), device_type, location_summary, source = "QR" And attempts to update or delete an existing audit entry are rejected and logged And any subsequent corrections or status changes produce new audit entries linked by audit_chain_id And audit entries are retrievable via admin UI/API and cannot be modified via user actions
Correct Session Resolution from Token
Given a QR token encoding attendee_id and session_id = S When the token is scanned Then mark Present only on session S for that attendee And do not alter any other sessions for the attendee And if session S does not exist or does not belong to the attendee, respond 404 with error_code = "SESSION_TOKEN_MISMATCH" and do not mark attendance
Operator Context Capture (Kiosk vs Staff)
Given the scanner is operating in Kiosk Mode without a logged-in staff user When a scan succeeds Then set operator_context = "kiosk" and persist kiosk_id, with operator_id = null Given the scanner is operating in Staff Mode with an authenticated staff user When a scan succeeds Then set operator_context = "staff" and persist operator_id = staff_user_id And the response payload includes operator_context for observability
On-Scan Consent & Intake
"As a practitioner, I want the check-in flow to collect any missing client data and consents so that records stay compliant without extra admin work."
Description

After identification, evaluate profile completeness and consent currency against workspace rules. If required, present dynamic forms (intake, policies, HIPAA/GDPR consent) with versioning, e-sign capture, and time-stamped audit trails. Support completion on the kiosk with privacy mode or via a secure link to the attendee’s device (SMS/email) to reduce queue congestion. Map responses to structured client fields, attach signed PDFs to the client record, and block attendance finalization until mandatory items are completed (configurable). Provide multilingual content, conditional logic, and autosave.

Acceptance Criteria
On-Scan Rules Evaluation and Required Forms Prompt
Given an attendee scans a valid QR code and is identified, When workspace rules for profile completeness and consent currency are evaluated, Then the system determines within 500 ms whether any mandatory forms are required for this session (service type, location, and role specific). Given no required items are missing or expired, When evaluation completes, Then the attendee is advanced directly to attendance finalization with zero forms displayed. Given one or more required items are missing or expired, When evaluation completes, Then the system presents only the required forms in the configured order and displays a progress indicator of remaining items.
Consent Versioning and Currency Enforcement
Given a policy/intake with version V is marked current, And the attendee previously signed version < V, When they check in, Then the attendee must re-consent to version V before attendance can be finalized. Given a policy/intake is updated, When attendees who signed an older version attempt to check in, Then the form displays the new version ID and publish timestamp, and the submission records version ID and a content hash in the audit log. Given no consent is required by rules for this attendee/session, When evaluation runs, Then no consent forms are presented and the audit log records the policy versions checked.
E‑Sign Capture and Audit Trail
Given a signature field is present, When the attendee signs (typed or drawn) and submits, Then the system captures signer ID, signature artifact, UTC timestamp, and device/IP metadata and binds them to the specific form version in an immutable audit record. When the form is submitted, Then a tamper-evident PDF is generated embedding the signature, version metadata, and timestamps, and a checksum is stored to detect later modification. Given an audit record exists, When retrieved by authorized staff, Then it is viewable within 2 seconds and shows who signed, when, what was agreed to (versioned text), and the acceptance evidence.
Kiosk Privacy Mode and Device Hand‑Off
Given privacy mode is enabled, When a form with PII is on-screen, Then the kiosk auto-blurs after 20 seconds of inactivity and hides prior answers until the attendee re-authenticates by rescanning their QR code. Given the attendee opts to continue on their own device, When staff selects Send to device, Then a single-use encrypted link is sent via SMS/email, expiring after 30 minutes or upon first use, and the kiosk shows a waiting screen with no PII. Given the attendee completes the form on their device, When submission succeeds, Then the kiosk advances the check-in flow within 3 seconds and the secure link becomes invalid.
Structured Field Mapping and PDF Attachment
Given form fields are mapped to client profile fields, When the attendee submits, Then values are validated against type and constraints and saved to the corresponding structured fields; unmapped responses are stored as form artifacts. When submission succeeds, Then a signed PDF copy is attached to the client record under Documents with filename convention {ClientLast}_{ClientFirst}_{FormName}_v{Version}_{YYYYMMDD}.pdf and is retrievable within 2 seconds. Given the same form instance is submitted multiple times before attendance is finalized, When a duplicate submission occurs, Then the existing draft record is updated idempotently rather than creating a duplicate.
Attendance Finalization Blocker (Configurable)
Given Block attendance until mandatory items complete is enabled, When any required form is incomplete or any required consent is expired, Then the Finalize attendance action is disabled and a message lists the specific pending items. Given all mandatory items are complete, When the attendee returns to check-in, Then Finalize attendance becomes available without requiring re-entry of already captured data. Given the blocker setting is disabled, When required items are incomplete, Then attendance can be finalized and the session record logs a warning noting the unmet items.
Multilingual, Conditional Logic, Autosave, and Offline
Given the attendee’s language preference or kiosk language setting, When forms render, Then all labels, help text, error messages, and policy content appear in the selected language, including correct right-to-left layout where applicable. Given conditional logic rules, When a trigger answer is provided, Then dependent fields/pages show or hide immediately without page reload and hidden fields are excluded from submission and storage. Given autosave is enabled, When the attendee types for 10 seconds or navigates between pages, Then progress is saved; if interrupted, resuming via kiosk or secure link restores the last saved state exactly. Given the kiosk is offline, When the attendee completes forms, Then data is encrypted locally and queued with idempotency keys; upon reconnection, submissions sync in order with preserved timestamps and no duplicates or data loss.
Instant Paylink Launch
"As an attendee, I want a paylink to launch right after check-in so that I can pay immediately and avoid invoices later."
Description

Immediately launch a payment flow post-check-in based on workspace rules (e.g., deposit, copay, outstanding balance). Generate a secure paylink tied to the session/invoice draft, supporting card, wallet, and ACH where enabled. Detect existing payments to avoid double charges and provide a quick-dismiss or “pay later” path if configured. Show real-time payment status back to the kiosk/admin and finalize the invoice upon success, triggering receipts and automations. Support SCA/3DS, surcharge/tip options, and refunds/voids via standard SoloPilot payments integration.

Acceptance Criteria
Auto-launch paylink after QR check-in
Given the workspace has Instant Paylink Launch enabled and the attendee scans a valid QR code for a scheduled or in-progress session at the kiosk When the system confirms the attendee’s identity and marks attendance Then a secure, single-use paylink bound to the session and invoice draft is generated and displayed within 2 seconds And the paylink token is signed and expires after 15 minutes if unpaid And the paylink pre-fills client, session, and amount fields according to workspace billing rules
Amount, methods, tips, and surcharges per workspace rules
Given workspace billing rules define amount logic (deposit, copay, outstanding balance, remainder), enabled payment methods (card, wallet, ACH), tip options, and surcharge policy When the paylink is rendered Then the amount due is calculated by precedence: outstanding balance > copay > deposit > remainder, capped by the session fee and never negative And only enabled payment methods are shown; Apple Pay/Google Pay appear only on supported devices/browsers; ACH is shown only where enabled and supported And tip options (off/percent/custom) appear per settings and are applied before surcharges; tip selection is optional unless configured as required And surcharge (if enabled) is disclosed with amount and legal text prior to confirmation and applied only to eligible methods per policy
Existing payment detection and idempotency
Given an existing successful payment fully covers the session/invoice amount When the attendee checks in or opens the paylink Then the system suppresses the payment flow and displays “Payment already received” with a Close action And the admin console reflects Paid status for the session within 2 seconds Given a partial payment exists When the paylink is generated Then the amount due equals the remaining balance only And all payment attempts use an idempotency key derived from the invoice ID so concurrent submissions do not double charge
Pay later dismissal path
Given Allow Pay Later is enabled for the workspace When the client taps Pay later from the paylink Then the paylink closes within 1 second and the session remains checked in And an unpaid invoice draft is retained and a follow-up paylink is sent by the configured channel(s) within 60 seconds And the admin console shows Pay later status in real time and the session appears in the Collections/Unpaid list
Realtime payment status and invoice finalization
Given a client submits a payment via the paylink When the processor returns Succeeded Then the kiosk shows “Payment received” within 2 seconds and the admin console updates within 2 seconds And the invoice is finalized (draft -> issued/paid), a receipt is sent to the client, and configured automations fire When the processor returns Failed or Canceled Then the paylink shows a clear failure message with reason (if available) and offers Retry or Pay later (if enabled), and the admin console reflects the failure within 2 seconds
SCA/3DS authentication handling
Given the selected card requires SCA/3DS When the client authorizes the transaction Then the paylink presents the challenge inline or via redirect and on completion resumes to the correct success or failure state without losing context And if the challenge is not completed within 5 minutes, the payment is marked RequiresPaymentMethod and the client is returned to method selection with an explanatory message
Refunds and voids via standard payments integration
Given a payment was captured via Instant Paylink Launch When an admin initiates a void before settlement Then the payment is voided through the standard SoloPilot payments integration, the invoice reopens to Unpaid, and the client is notified When an admin initiates a refund after settlement Then a full or partial refund is processed via the standard integration, the invoice reflects the refunded amount, configured automations fire, and both admin and client receive a refund receipt And all refund/void events are audit-logged with actor, timestamp, amount, and reason
Admin Queue Monitor & Overrides
"As a practitioner, I want a real-time check-in dashboard and overrides so that I can handle exceptions and keep the event moving."
Description

Provide a real-time dashboard listing upcoming sessions, check-in status, waitlist, and exceptions (invalid/expired QR, missing consents, declined payments). Allow staff to manually check in attendees, reissue QR codes, edit session assignments, and bypass steps with role-based permissions. Surface throughput metrics (avg. scan time, peak load), exportable logs, and alerts for stalls or high failure rates. Include quick actions to resend reminders/paylinks and to annotate attendance notes. All admin actions are audited for compliance and reporting.

Acceptance Criteria
Real-Time Queue Dashboard Updates
Given the dashboard is open on Today + Next 24h When an attendee scans a valid QR code Then the attendee’s status updates to "Checked-In" within 2 seconds without manual refresh And the session counters (Scheduled, Waiting, Checked-In, Exceptions) reflect the change within 2 seconds Given an invalid or expired QR event is received When the dashboard is visible Then the attendee row is tagged "Exception" with reason (Invalid_QR | Expired_QR) and a timestamp And the exception filter shows an increment within 2 seconds
Manual Check-In Override With Role-Based Permissions
Given a user has role Admin or Front Desk with permission attendance.override When they select Manual Check-In for an attendee blocked by missing consent or declined payment Then the system requires a confirmation and a reason (minimum 10 characters) And the attendee is marked Checked-In with a visible bypass tag (Consent Bypassed | Payment Bypassed) And an audit log is written with user id, role, attendee id, session id, previous state, new state, reason, timestamp, and IP/device Given a user lacks permission attendance.override When they attempt Manual Check-In Then the action is denied with a permissions error and no state change or audit entry is recorded
Reissue QR Code From Dashboard
Given an attendee is scheduled and not checked in When staff clicks Reissue QR Then a new QR token is generated and all prior tokens for that session are invalidated And the new QR is delivered via configured channels (SMS/Email) within 10 seconds And delivery status displays as Sent or Failed with a Retry option And an audit entry records token reissue with actor, channels, and outcome
Edit Session Assignment and Waitlist Promotion
Given capacity exists in the target session When staff changes an attendee’s session assignment Then the attendee is moved to the target session and dashboard counts update immediately And an audit entry records from_session, to_session, actor, and timestamp Given the target session is at capacity When staff attempts the move Then the change is blocked with a capacity error and no state change occurs Given an attendee is on the waitlist When staff promotes the attendee Then status becomes Scheduled and (if enabled) a notification is sent, with delivery outcome shown
Throughput Metrics and Alerts
Given check-ins have occurred in the last 15 minutes When viewing the metrics panel Then average scan-to-check-in time, 95th percentile, peak concurrent scans, and failure rate are displayed with a 15-minute window label And metrics refresh at least every 15 seconds Given failure rate exceeds 5% for 5 consecutive minutes or no successful scans occur for 2 minutes while 5 or more attendees remain Waiting Then an alert banner is shown and a notification is sent to configured recipients And the alert includes the triggering metric and time window
Exportable Admin Logs With Filters
Given a date-time range and filters (action types, actor, session, outcome) are applied When Export CSV is clicked Then a CSV is generated within 10 seconds containing one row per action with columns: timestamp (ISO 8601), actor id, actor role, action, attendee id, attendee name, session id, session start, previous state, new state, reason/note, outcome, IP/device And the number of rows equals the on-screen filtered results And timestamps in the export reflect the workspace timezone
Quick Actions: Resend Reminders/Paylinks and Annotate Notes
Given an attendee row is selected When staff clicks Resend Reminder or Resend Paylink Then the message is sent via configured channels with current deep links and delivery status shown (Sent | Failed) And an audit entry records the action and outcome When staff adds an attendance note up to 500 characters Then the note is saved to the session attendance record and the client timeline with author, timestamp, and visibility per workspace settings

Auto Paylinks

Generates personalized, per‑attendee paylinks on attendance, late arrival, or completion—prefilled with ticket type, taxes, and currency. Supports Apple/Google Pay, vouchers, and expiry windows with smart retries. Delivers via email/SMS/DM for faster conversion and fewer manual chases.

Requirements

Event-Triggered Paylink Generation
"As a solo practitioner, I want paylinks to be generated automatically when a client attends or completes a session so that I can eliminate manual billing and get paid faster."
Description

Build a rules-driven service that generates a unique, personalized paylink for each attendee automatically on qualifying events (check-in, late arrival, session completion). The link is scoped to the session and attendee, associates the correct service and ticket type, and supports group sessions. Provide idempotent generation per event to avoid duplicates and expose workspace and per-service controls to configure which events trigger links and overrides. Persist the paylink object with metadata for delivery, expiry, vouchers, and reconciliation, and attach it to the session and draft invoice within SoloPilot.

Acceptance Criteria
Idempotent Paylink Generation per Event
Given a qualifying event (check-in, late arrival, or completion) with event_id E for attendee A in session S When the paylink generation is invoked one or more times for the same (A, S, E) Then exactly one paylink object exists for (A, S, E) And subsequent invocations return the existing paylink identifier without creating new records And the draft invoice has no duplicate line items or attachments for (A, S, E)
Accurate Prefill: Service, Ticket, Taxes, Currency
Given service X with ticket type T configured at price P and tax rules R in currency C And attendee A is associated to session S of service X When a qualifying event triggers paylink generation Then the paylink payload includes service_id X, ticket_type_id T, session_id S, attendee_id A And currency equals C And tax amounts are computed per R and match the workspace rounding rules And total equals P plus applicable taxes And the paylink label references T and S
Trigger Configuration: Workspace Defaults and Service Overrides
Given workspace defaults enable events {check-in, late arrival, completion} And service X overrides to enable only {completion} When events occur for attendees in sessions of service X Then paylinks are generated only on completion events And no paylinks are generated on check-in or late arrival And changing the service override to {check-in, completion} affects subsequent events without code changes And disabling all events at service level results in no paylinks for that service
Group Sessions: Per-Attendee Link Creation
Given a group session S with attendees A1, A2, A3 and mixed ticket types When a qualifying event occurs for the session (e.g., check-in) Then one distinct paylink is created for each attendee that meets the trigger condition And for a late arrival event only late attendees receive paylinks And each paylink amount reflects that attendee’s ticket type and taxes And paylinks are attached to each attendee’s draft invoice without cross-attaching to other attendees
Persistence and Attachment: Session and Draft Invoice
Given paylink generation succeeds for attendee A in session S When inspecting the data store Then a paylink record exists with a stable id, created_at, event_type, event_id, service_id, session_id, attendee_id, ticket_type_id, currency, amounts, status=Active And the paylink is attached to session S And a draft invoice for attendee A contains a line item referencing the paylink id and amounts And retrieving session S lists the paylink under its payments metadata
Scoping and Uniqueness for Paylinks
Given a paylink L created for attendee A and session S When attempting to associate L with a different attendee or session Then the operation is rejected And L’s token is unique and non-guessable (>=128 bits of entropy) And L contains scope metadata {attendee_id, session_id, service_id, event_id} to enforce usage limits
Metadata: Delivery Channels, Expiry, Vouchers, Reconciliation
Given workspace default expiry window W hours and delivery channels {email, SMS} and vouchers enabled with voucher_set V When a paylink is generated Then the paylink includes expiry_at = created_at + W with timezone awareness And includes delivery metadata with resolved recipient endpoints for the enabled channels And includes voucher metadata voucher_set_id=V and allows_vouchers=true And includes reconciliation fields {draft_invoice_id, session_id, attendee_id, service_id, event_id} And includes smart_retry policy metadata with a schedule derived from workspace defaults
Prefilled Pricing & Tax Engine
"As a business owner, I want the paylink to prefill the correct price, currency, taxes, and any vouchers so that clients can pay the exact amount without confusion or back-and-forth."
Description

Implement a pricing and tax engine that pre-populates line items, discounts/vouchers, surcharges (e.g., late arrival fee), currency, and tax calculations per locale for each paylink. Support per-service price books, attendee-specific voucher codes, inclusive/exclusive tax models, rounding rules, and multi-currency display with automatic currency selection based on client profile and session location. Expose workspace-level settings and per-service overrides. Persist the computed quote total (amount due and itemized breakdown) and an integrity hash to prevent tampering and enable reconciliation.

Acceptance Criteria
Settings Precedence: Workspace vs Per‑Service Overrides
- Given the workspace default tax model is Inclusive and a service override sets Exclusive, When a paylink is generated for that service, Then the Exclusive tax model is used for all calculations. - Given the workspace rounding rule is Half‑Up and a per‑service override sets Bankers rounding, When generating the quote, Then Bankers rounding is applied to all line items and totals. - Given no per‑service override exists for a setting, When generating the quote, Then the workspace default setting is used. - Given the workspace default is changed after a quote has been generated, When the existing paylink is retrieved, Then the persisted quote remains unchanged by the new default.
Per‑Service Price Book Application to Paylink
- Given a service has a price book with a base price and defined surcharge amounts, When a paylink is generated, Then the service base price is added as a line item and any triggered surcharge (e.g., late arrival) is added as a separate line item. - Given locale tax rules apply, When the paylink is generated, Then taxes are calculated according to the selected tax model and shown itemized or totaled per workspace configuration. - Then Amount Due equals the sum of line items plus taxes minus discounts, consistent with configured rounding rules.
Attendee‑Specific Voucher Code Application
- Given attendee A has an assigned, valid voucher code, When a paylink is generated for attendee A, Then the voucher automatically applies to eligible line items and is shown as a discount line. - Given a voucher is attendee‑specific, When attendee B attempts to use attendee A’s code, Then the discount is rejected and totals remain unchanged. - Given a fixed‑amount voucher exceeds the pre‑tax (exclusive model) or tax‑inclusive (inclusive model) subtotal, When applied, Then the discount is capped at the subtotal and Amount Due is not negative. - Given the tax model is Exclusive, When a voucher is applied, Then taxes are computed on the discounted taxable base; Given the tax model is Inclusive, Then the tax component is reduced proportionally with the discount. - Given a voucher is expired or has reached its usage limit, When the paylink is generated, Then no discount is applied and the voucher is flagged as invalid.
Locale Tax Model: Inclusive vs Exclusive
- Given a locale configured for Inclusive tax (e.g., 15%), When a paylink is generated, Then item prices are displayed tax‑inclusive, the tax component is shown separately, and Amount Due equals the tax‑inclusive subtotal (subject to discounts/surcharges). - Given a locale configured for Exclusive tax (e.g., 10%), When a paylink is generated, Then tax is added on top of the net price and shown separately, and Amount Due equals net subtotal plus tax (subject to discounts/surcharges). - Given session location determines locale, When generating a paylink, Then the correct locale’s tax model and rates are used.
Rounding Rules and Currency Minor Units
- Given currency JPY (0 minor units), When computing the quote, Then all amounts are rounded to whole yen and no fractional units are displayed. - Given currency USD (2 minor units) with Half‑Up rounding, When fractional cents occur, Then line items and totals are rounded per rule and the sum of rounded lines equals the rounded total (no penny drift). - Given a per‑service rounding override differs from the workspace default, When generating a paylink for that service, Then the override is used for all calculations and display.
Multi‑Currency Auto Selection and Display
- Given a client profile has a preferred currency supported by the service’s price book, When generating a paylink, Then that preferred currency is selected. - Given the client has no supported preferred currency, When generating a paylink, Then the currency is selected based on session location; if unsupported, the workspace default currency is used. - Then all displayed amounts use the selected currency’s symbol/ISO code and locale‑appropriate formatting and minor units.
Quote Persistence and Integrity Hash
- Given a paylink is generated, When the quote is retrieved later, Then the itemized breakdown and Amount Due are identical to the originally computed values regardless of subsequent configuration or price book changes. - Given an integrity hash is stored for the quote payload, When any covered field (e.g., line items, taxes, currency, totals, expiry) is altered client‑side, Then the server rejects the request, logs an integrity violation, and keeps the original quote unchanged. - Given a successful payment, When reconciling the transaction, Then the settled amount matches the persisted Amount Due and the integrity hash verification passes. - Given a paylink is resent or retried, When generated for the same session/attendee context, Then the same quote ID and hash are reused (idempotent) and no duplicate quotes are created.
Wallet & Card Payment Support
"As a client, I want to pay via Apple Pay or Google Pay with one tap so that checkout is fast and effortless on my device."
Description

Integrate Apple Pay and Google Pay with secure card entry fallback to enable one-tap checkout from the paylink. Support 3DS2/SCA where required, optional vaulting per workspace, localized currency/tax presentation, and voucher redemption in-flow. Include device/browser capability checks, merchant domain verification, country/brand restrictions, and graceful degradation to card form. Provide success/cancel callbacks that redirect to SoloPilot receipts or scheduling pages and emit standardized events for reconciliation.

Acceptance Criteria
Wallet Availability and Merchant Verification Gate
- Given a paylink is opened on an Apple Pay–capable device with a verified merchant domain, When the checkout loads, Then the Apple Pay button is displayed and enabled. - Given a paylink is opened on a Google Pay–capable browser with supported card networks and an allowed merchant country, When the checkout loads, Then the Google Pay button is displayed and enabled. - Given the device/browser does not support a wallet or merchant verification fails, When the checkout loads, Then no wallet buttons are shown and the secure card form is displayed. - Given workspace or processor brand/country restrictions apply, When the checkout loads, Then only permitted wallet options are shown and restricted options are hidden.
Secure Card Entry and SCA Enforcement
- Given wallets are unavailable or the user selects Pay with card, When the card form renders, Then fields are hosted (iFrame) and PCI-sensitive data never touches SoloPilot servers. - Given a payment requires SCA per 3DS2 rules, When the user submits the card form, Then a 3DS2 flow is initiated and the payment continues only after successful authentication or frictionless approval. - Given a 3DS2 challenge is canceled or fails, When the payment attempt completes, Then the payment is declined with a clear error and the user remains on checkout with retry options. - Given the issuer indicates no SCA is required, When the payment is submitted, Then the payment is authorized without challenge and the success callback fires.
Voucher Redemption In-Flow
- Given a valid voucher covers the full balance, When the user applies the code, Then the total becomes 0 and checkout completes without card or wallet. - Given a valid voucher covers part of the balance, When the user applies the code, Then the remaining balance recalculates (including taxes) and the user can complete with wallet or card. - Given an expired, invalid, or consumed voucher, When the user applies the code, Then an error is shown and totals remain unchanged. - Given voucher application changes totals, When amounts render, Then a voucher line item and updated tax basis are displayed accurately.
Localized Currency and Tax Presentation
- Given the paylink currency and workspace locale, When the checkout loads, Then all amounts display in the paylink currency with locale-appropriate symbols, separators, and ISO code. - Given tax is configured as inclusive, When totals render, Then tax is displayed as included and totals reflect accurate rounding to the currency minor unit. - Given tax is configured as exclusive, When totals render, Then subtotal, tax, and total are shown as separate lines with accurate rounding to the currency minor unit. - Given a voucher is applied, When totals update, Then tax recalculates per rules and the display remains localized and correct.
Optional Card Vaulting with Consent
- Given the workspace has vaulting enabled, When the card form renders, Then a save-payment-method checkbox with clear consent language is shown unchecked by default. - Given the user provides consent and payment succeeds, When the gateway returns a reusable token, Then the token is stored to the attendee profile with brand, last4, and expiry (no PAN) for future use. - Given the workspace has vaulting disabled or jurisdiction disallows it, When the checkout loads, Then no save-payment option is shown and no token is stored. - Given the user does not provide consent, When the payment completes, Then no payment method is stored.
Success/Cancel Callbacks and Redirects
- Given a successful authorization/capture, When the transaction completes, Then the browser redirects within 3 seconds to the SoloPilot receipt URL with paylink_id, payment_intent_id, and status=success. - Given the user cancels a wallet sheet or card checkout, When the cancellation is confirmed, Then the browser redirects to the SoloPilot scheduling/origin page with status=cancel and a reason code. - Given a success or cancel occurs, When the callback fires, Then standardized payload data is available to the frontend to render confirmation or cancellation states without error. - Given a callback URL is unreachable, When redirect is attempted, Then a retry occurs once and a fallback on-page confirmation/cancellation view is shown.
Reconciliation Event Emission
- Given a paylink is opened, When checkout initializes, Then a paylink.initiated event is emitted with idempotency_key, paylink_id, workspace_id, and timestamp. - Given a payment is authorized, When authorization succeeds, Then a payment.authorized event is emitted with amount, currency, method, wallet_type, and 3ds_result. - Given a payment is captured or settled, When capture succeeds, Then a payment.succeeded event is emitted including voucher_applied, tax_amounts, and fee fields; and on failure/cancel, payment.failed or payment.canceled is emitted with failure_code and message. - Given events are emitted, When delivery occurs, Then events are signed, retried with exponential backoff for up to 1 hour, and stored in audit logs for 90 days.
Multi-Channel Paylink Delivery
"As a practitioner, I want paylinks sent via the best channel for each client with reliable delivery and tracking so that I reduce manual follow-ups and speed up payments."
Description

Deliver paylinks via email, SMS, and direct message channels based on client preferences and deliverability. Provide localized, templated messages with personalization tokens (name, amount, due date), a link shortener with click tracking, and channel-specific throttling. Implement fallbacks (e.g., SMS if email bounces), queueing with retries, and scheduling (immediate on trigger or delayed). Allow per-service and global templates. Log all delivery and engagement events to the client timeline in SoloPilot.

Acceptance Criteria
Preference-Based Localized Multi-Channel Delivery
Given a client profile with channel preferences prioritized (e.g., SMS > Email > DM), a locale and timezone, and a valid paylink payload including {client_name}, {amount}, {due_date} And per-service and global templates exist with localized variants When Auto Paylinks triggers a paylink delivery for that client Then the system selects the highest-ranked preferred channel that is enabled for the client, not opted-out, and currently deliverable And the selected message uses the per-service template matching the client locale; if absent, it falls back to the global template for that locale; if absent, it falls back to the default locale And the message renders with all personalization tokens fully substituted and correctly formatted for the client locale (currency, date, number formats) And no unresolved tokens (e.g., {{token}}) remain in the final message And the message subject/preview includes the amount and due date where applicable
Automatic Channel Fallback On Failure
Given email is the selected channel for delivery And the message is queued and attempted When the provider returns a hard bounce, spam rejection, or final delivery failure after max retries Then the system automatically falls back to the next available channel in the client's preference order (e.g., SMS, then DM) And the fallback send occurs within 5 minutes of detecting the failure And duplicate messages are prevented via an idempotency key per trigger-client pair And both the failed attempt and the fallback attempt are recorded with statuses and reasons
Shortened Paylink With Unique Click Tracking
Given a paylink URL is generated for a client and channel When the message is composed Then the paylink is wrapped in a shortened, unique tracking URL associated to the client, channel, and trigger And a click on the shortened URL resolves to the original paylink without altering query parameters critical to payment And each click event records timestamp, channel, client, and user agent metadata And multiple clicks from the same client within 60 seconds are de-duplicated for engagement metrics And click events are attributed back to the originating delivery attempt
Channel-Specific Throttling And Rate Limits
Given channel-level limits are configured (e.g., Email: 50 msgs/min per sender; SMS: 1 msg/sec per number; DM: platform-specific caps) And recipient-level daily caps are defined (e.g., max 3 paylink messages per 24 hours per recipient across all channels) When a burst of paylink deliveries is triggered Then the dispatcher enforces channel-specific throughput so provider limits are not exceeded And messages exceeding limits are queued and released as capacity becomes available And recipient-level caps prevent additional sends, marking them as suppressed with a suppression reason And suppression and rate-limited queue delays are recorded per attempt
Reliable Queueing With Retries And Backoff
Given a delivery attempt fails with a transient error (e.g., 429/5xx or network timeout) When the system schedules a retry Then it retries with exponential backoff and jitter (e.g., ~1m, ~5m, ~20m ±20%) up to a maximum of 3 attempts per channel And it stops retrying on permanent errors (e.g., 4xx hard failures) and marks the attempt as final-failed And idempotency ensures only one message is sent per trigger-client-channel despite retries And all retry attempt metrics (attempt number, delay, error code) are captured
Immediate And Scheduled Delivery Windows
Given a service is configured to send paylinks immediately on trigger or at a scheduled future time When an immediate send is requested Then the message is dispatched to the provider within 30 seconds of the trigger time When a scheduled send_at timestamp is provided in the service timezone Then the message is not sent before send_at and is dispatched within 5 minutes after that time And canceling or updating a scheduled send before send_at prevents the original send and applies the update And schedule adherence is recorded with planned vs actual send timestamps
Comprehensive Timeline Logging For Delivery And Engagement
Given any paylink delivery lifecycle event occurs (queued, sent, delivered, bounced, failed, suppressed, clicked) When the event is generated by the system or received via provider webhook Then an entry is appended to the client's SoloPilot timeline with: event type, channel, template ID and version, provider message ID, attempt number, timestamps, and outcome/reason codes And the entry links to the associated invoice/session and the exact message variant (content hash or render ID) And events are written idempotently to avoid duplicates when webhooks are retried And timeline entries are searchable by client, channel, status, date range, and provider message ID
Expiry Windows & Smart Retries
"As a practitioner, I want paylinks to expire and automatically remind clients before expiry so that I maintain urgency without spamming."
Description

Allow configuration of paylink expiry windows (e.g., 24–72 hours or end-of-day for late arrivals) with a visible countdown on the pay page. Schedule adaptive reminders prior to expiry and for unpaid states with channel rotation, quiet-hour and timezone awareness, and a maximum attempt cap. Automatically reissue a fresh link on expiry when allowed, invalidate the prior token, and suppress reminders after successful payment. Prevent duplicate notifications across channels and respect client opt-outs.

Acceptance Criteria
Configurable Expiry Window With Visible Countdown (24–72h and EOD for Late Arrivals)
Given an admin sets a default expiry window of 48 hours and a late-arrival policy of end-of-day in the client’s timezone And a paylink is generated at 10:00 client local time for a standard attendance When the recipient opens the pay page Then a countdown displays time remaining until 48 hours post-generation with HH:MM:SS precision and updates at least once per second And when a paylink is generated for a late arrival at 21:15 client local time on the same day Then the countdown displays time remaining until 23:59:59 that day in the client’s timezone And when the current time surpasses the configured expiry Then the pay page state switches to “expired” within 60 seconds and payment submission is blocked
Adaptive Reminder Schedule Before Expiry (Timezone- and Quiet-Hour-Aware)
Given quiet hours are configured as 20:00–08:00 in the recipient’s timezone And reminders are configured at T-12h and T-1h before paylink expiry And a paylink is set to expire at 09:00 in the recipient’s timezone When the system schedules reminders Then the T-12h reminder is deferred to 08:00 (start of next allowable window) on the expiry day And the T-1h reminder is scheduled at 08:00 on the expiry day And no reminder is sent during quiet hours And all reminder timestamps in logs are recorded in recipient local time and UTC
Channel Rotation Without Duplicate Notifications
Given channel order is Email → SMS → DM with per-attempt rotation and a 10-minute cross-channel deduplication window And Attempt 1 is scheduled at T1 When Email for Attempt 1 is successfully sent Then SMS and DM are not sent for Attempt 1 And Attempt 2 at T2 uses SMS as the primary channel And if the primary channel for an attempt hard-fails (e.g., bounce), the system retries the same attempt on the next channel within 2 minutes, still sending at most one message successfully per attempt And no two messages for the same paylink are sent within the deduplication window across channels
Maximum Reminder Attempt Cap Enforcement
Given the maximum reminder attempts per paylink is set to 4 When the system evaluates scheduling after 4 sent or acknowledged attempts Then no further reminders are scheduled or sent for that paylink And an audit log entry "reminder cap reached" is recorded with timestamp and attempt count And the paylink detail UI shows status "Reminder cap reached" And if a new paylink is reissued, its attempt counter starts at 0 independently of the prior link
Automatic Reissue on Expiry with Prior Token Invalidation
Given the policy "Allow reissue on expiry" is enabled And a paylink expires at time T When the system processes expiry Then a new paylink with a different token is generated within 60 seconds of T And the old token immediately returns HTTP 410 Gone with reason "expired" and cannot be used to pay And delivery of the new paylink is scheduled on the next allowable channel/time respecting quiet hours and opt-outs And future reminders reference only the new paylink URL And audit logs link old and new token IDs with timestamps
Reminder Suppression After Successful Payment
Given a paylink has pending scheduled reminders When the recipient completes payment via any supported method Then all pending reminders for that paylink are cancelled within 60 seconds And no additional reminders are sent after payment, including those triggering concurrently within a 1-minute race window And audit logs record the cancellation of each pending reminder with reason "paid"
Respect Channel Opt-Outs and Consent
Given a recipient has opted out of SMS and DM but not Email When the system schedules or sends reminders Then only Email is used for attempts and other channels are excluded And if all channels are opted out, no reminders are sent and the paylink is marked "no allowed channels" for notifications And opt-out preferences are enforced consistently across reissued paylinks and all attempts And audit logs record the channels excluded due to opt-out
Secure One-Time Link Tokens
"As an operator, I want paylinks to be secure, single-use, and tamper-proof so that I can prevent fraud and protect client data."
Description

Generate signed, expiring, single-use tokens embedded in paylinks, binding attendee id, session id, invoice/quote id, and an amount hash. Enforce one-time redemption, rate limiting, replay detection, and TLS-only endpoints. Store issuance/access/redemption audit logs and provide admin revocation and regeneration actions. Keep PII out of URLs and integrate with fraud and risk signals from the payment processor to block suspicious attempts.

Acceptance Criteria
Secure Token Structure and Transport
Given a paylink is generated for an attendee-session-invoice When the token is created Then the token includes claims: attendee_id, session_id, invoice_or_quote_id, amount_hash, currency, iat, exp, jti and is signed with a server-held key And amount_hash equals a server-computed digest of amount_in_minor_units + currency + applicable_taxes + voucher_id (if present) And any modification to any claim or the amount invalidates the signature and causes validation to fail with HTTP 403 Given a paylink URL is constructed Then the URL uses the https scheme and includes only a single opaque token parameter (and optional non-PII routing like locale) And the URL contains no PII such as name, email, phone, or address And the platform never generates http links for token endpoints And successful responses include Strict-Transport-Security with max-age ≥ 15552000, includeSubDomains, preload And the endpoint accepts only TLS ≥ 1.2 handshakes
Redemption Controls and Abuse Prevention
Given a valid, unredeemed token When a payer completes payment using the paylink Then the token is atomically marked redeemed with redeemed_at, actor, and payment_reference And the charge is executed with an idempotency key derived from jti to prevent duplicate capture Given the same token is submitted again after redemption When the redemption endpoint is called Then no additional charge is attempted And the response is HTTP 409 with reason token_already_redeemed And an audit event is recorded with type replay_attempt Given validation or redemption is attempted repeatedly for the same token When requests exceed 10 validations per minute per IP or 3 redemptions per minute per attendee_id (configurable thresholds) Then the service returns HTTP 429 with a Retry-After header And no charge is attempted while rate-limited Given a request reuses the same jti within a rolling window When the nonce/jti is detected as previously seen Then the request is rejected as a replay with HTTP 409 and logged
Expiry, Revocation, and Regeneration
Given a token has an exp claim When current_time > exp Then validation and redemption fail with HTTP 410 and no charge is attempted And an audit event is recorded with reason token_expired Given an admin revokes a token prior to redemption When the revoked token is accessed or redeemed Then the request fails with HTTP 410 and reason token_revoked And no charge is attempted And the audit log records admin_id, revoked_at, and optional reason Given an admin regenerates a paylink for the same attendee/session/invoice When regeneration is performed Then a new token with a new jti and exp is issued and the prior token is marked revoked And any access to the old token returns HTTP 410 And the new token binds to the same entities and updated amount_hash (if amount changed)
Auditability and Fraud Blocking
Given any token lifecycle event (issue, access, validate, redeem, revoke, regenerate, failure) When the event occurs Then an audit record is stored with: timestamp_utc, event_type, token_jti, attendee_id, session_id, invoice_or_quote_id, request_ip, user_agent, http_status, outcome, reason_code, payment_reference (if any) And logs do not store PII in URLs or token payloads And audit records are immutable and tamper-evident Given the payment processor returns a high-risk or block signal for the attempt When redemption is initiated Then the system blocks the attempt before capture, returns HTTP 403 with reason fraud_blocked, and records fraud_provider, risk_score, and provider_reference in the audit And the token remains unredeemed and subject to a cooldown period (configurable) unless manually overridden by an admin Given a subsequent attempt is evaluated as low risk within the valid window When redemption is retried Then the payment proceeds and the token is redeemed
Payment Reconciliation & Auto-Invoicing
"As a practitioner, I want payments to automatically reconcile to invoices and sessions so that my books stay accurate without manual data entry."
Description

Process payment provider webhooks idempotently to mark invoices paid, attach receipts, update session financial status, and close or invalidate the paylink. Support partial payments, refunds, chargebacks, and voucher write-offs with clear ledger entries. Sync results to SoloPilot’s invoicing and reporting, trigger automated receipt emails, and optionally post to external accounting integrations when enabled. Surface reconciliation status and errors in the session timeline with operator retry actions.

Acceptance Criteria
Idempotent Webhook Processing - Duplicate Events
Given a payment provider webhook with event_id E already processed for invoice INV-123 When the same webhook is received again within the idempotency window Then the system returns 200 OK without side effects And no new ledger entries are created And the invoice, session financial status, receipts, and paylink state remain unchanged And an audit log entry notes "duplicate webhook ignored" with event_id E
Full Payment Reconciliation - Marks Invoice Paid and Closes Paylink
Given an open invoice with outstanding balance B and an active paylink linked to session S And a successful payment webhook P with amount == B, matching currency and invoice_id When the webhook is validated and applied Then the invoice status becomes Paid with paid_at timestamp set to P.settled_at And a receipt PDF is generated and attached to the invoice And a receipt email is sent to the billing contact within 60 seconds And session S financial status updates to Paid And the paylink transitions to Closed and becomes unusable And reporting totals and aging reflect the payment within the next data sync cycle (<5 min)
Partial Payment Allocation - Remaining Balance Maintained
Given an invoice with outstanding balance B and an active paylink And a validated payment webhook with amount A where 0 < A < B When the payment is reconciled Then a ledger entry allocates A against the invoice And the invoice status becomes Partially Paid with remaining balance B-A And a partial payment receipt is generated and emailed And the paylink remains Active with updated payable amount B-A And reporting reflects partial payment and remaining balance
Refund Processing - Full and Partial
Given an invoice with total paid amount P > 0 reconciled via provider transaction T And a refund webhook R referencing T with amount R.amount where 0 < R.amount <= P When the refund is validated and applied Then a refund ledger entry is created with proper sign and reference to R.id And the invoice balance increases by R.amount And the invoice status becomes Refunded if R.amount == P else Partially Paid And a refund receipt (credit note) is generated and emailed to the billing contact And the original paylink remains Closed and cannot be used for new charges
Chargeback Handling - Dispute Recorded and Paylink Invalidated
Given an invoice previously marked Paid via provider transaction T And a chargeback/dispute webhook D is received for T When the dispute is recorded Then a chargeback ledger entry is created for the disputed amount with reference D.id And the invoice status becomes Disputed And the session timeline shows a high-severity alert with dispute details and links to provider case And the associated paylink is Invalidated And reporting and aging reflect the disputed amount
Voucher Write-Off Application
Given an invoice with remaining balance B and an approved voucher V applicable to the invoice When a voucher write-off event for amount A where 0 < A <= B is applied Then a write-off ledger entry is created with reference V.code And the invoice balance decreases by A And the invoice status becomes Paid if A == B else Partially Paid And a receipt shows voucher applied and remaining balance And external accounting (if enabled) records the write-off in the configured account
External Accounting Sync and Error Retry with Timeline Surfacing
Given external accounting sync is enabled and credentials are valid And a reconciliation event occurs (payment, refund, chargeback, or write-off) When the system attempts to post the corresponding entry to the external system Then on success the invoice is tagged Synced with external_reference populated And on failure an error entry appears in the session timeline with the provider message and a Retry action And automatic retries occur up to 3 times with exponential backoff and idempotent payloads And a manual Retry from an operator triggers an immediate attempt and updates the timeline with outcome

Reconcile Sweep

One click to bulk reconcile the entire roster: capture outstanding payments, mark paid/partial/refunded, issue receipts, and export to your accounting tool. A clear dashboard surfaces exceptions (e.g., failed card, PO required) so you wrap the workshop in minutes, not evenings.

Requirements

One-Click Reconcile Sweep (with Dry Run)
"As a solo practitioner, I want to reconcile all sessions and invoices with one click so that I can close out my week in minutes without manual line-by-line updates."
Description

Provide a single action that bulk reconciles all eligible sessions and invoices within a selected date range or roster. The sweep first performs a dry run to preview proposed changes (counts, totals, and status transitions), flag conflicts, and estimate processing time. Upon confirmation, it applies updates transactionally, marking items paid/partial/refunded, creating any missing invoice links, and recording results with idempotency to prevent duplicates. Includes progress tracking, cancellation safeguards, and retry logic for transient gateway or API errors. Integrates with SoloPilot’s scheduling, notes, and invoicing so completed sessions automatically reconcile to billing, reducing manual handoffs and ensuring consistent financial state.

Acceptance Criteria
Dry Run Summary Preview for Selected Scope
Given I select a date range or a roster and click Reconcile Sweep When the system performs a dry run Then a preview displays item counts by action (mark paid, mark partial, mark refunded, create/link invoice), totals (charges, refunds, net, outstanding), and the number of exceptions And the estimated processing time is shown And no persistent changes are saved And currency and time zone reflect my workspace settings
Scope Filtering by Date Range or Roster
Given I select a date range only When I run the dry run Then only eligible sessions and invoices whose service dates fall within the range are included Given I select a roster only When I run the dry run Then only eligible sessions and invoices associated with that roster are included Given I select both a date range and a roster When I run the dry run Then only items that match both filters are included And items excluded by filters are listed under Excluded with reasons
Conflict and Exception Surfacing in Dry Run
Given conflicts exist (e.g., failed card, PO required, locked invoice, amount mismatch, missing client profile) When the dry run completes Then each conflicted item is flagged with a category, a human-readable reason, and a recommended next step And conflicted items are excluded from auto-apply counts And I can export the exception list as CSV And the Apply button remains enabled but shows a warning badge with the number of exceptions
Transactional Apply with Idempotency and Rollback
Given I confirm the dry run When the apply operation starts Then each item update is grouped in an atomic unit so that partial updates on that item do not persist on failure And an idempotency key tied to scope and parameters is used for all operations And re-running apply with the same parameters and idempotency key makes no additional changes And if the process is interrupted, already-committed items remain consistent and not duplicated upon resume
Status Transitions, Receipts, and Accounting Export
Given payments and refunds are present for included invoices When apply completes Then invoices with total payments >= total due and no pending refunds are marked Paid and receipts are issued to the client contact And invoices with 0 < payments < total due are marked Partial with the correct remaining balance And invoices with refunds are marked Refunded or Partially Refunded with accurate net balances, and refund receipts are issued And the number of items transitioned by status matches the dry run preview counts And if an accounting export integration is enabled, new/updated invoices and payments are exported exactly once with the same idempotency key
Missing Invoice Link Creation and Line Items
Given a completed session lacks an associated invoice When apply runs Then a new invoice is created or an existing draft/open invoice for that client is linked according to workspace rules And line items are generated from session data (service, quantity/duration, rate, taxes) matching configured templates And the session is linked to the invoice line item And re-running the sweep does not create duplicate invoices or line items
Progress Tracking, Retry Logic, Cancellation, and Final Report
Given the apply operation involves multiple items and external APIs When apply runs Then a live progress indicator shows processed/remaining counts and percent complete And transient errors (e.g., HTTP 429/503, timeouts) are retried with exponential backoff up to a configurable limit before marking the item failed And on user-cancel, in-flight operations finish, no new operations start, and the run is marked Cancelled Safely And a final report summarizes successes, failures (with error codes/messages), skipped/excepted items, and total elapsed time, and can be exported
Rules-Based Payment Matching Engine
"As a finance-focused user, I want payments to automatically match the right invoices using smart rules so that I minimize manual reconciliation and errors."
Description

Implement a deterministic rules engine that maps external payments and refunds to the correct client, session, and invoice using identifiers (invoice ID, client email, reference number), date/amount tolerances, memos, and payment metadata. Supports configurable matching priority, small-variance thresholds, duplicate detection, settlement vs. authorization states, and idempotent re-runs. Automatically allocates funds across multiple invoices when appropriate and supports fallback manual selection for ambiguous matches. Produces clear explanations for each match to aid auditability and user trust.

Acceptance Criteria
Exact Match via Invoice ID (Deterministic)
Given an incoming settled payment with metadata.invoice_id matching an open invoice ID exactly and amount equal to the invoice outstanding balance, When the engine runs, Then it maps the payment to that invoice, marks it paid in full, links to the correct client and session, and records a match explanation referencing rule 'invoice_id_exact' with evaluated fields. Given the same payment is reprocessed in a subsequent run, When the engine runs again, Then no duplicate allocations are created and the explanation indicates 'idempotent_reuse' of match id. Given the invoice is already fully paid, When such a payment arrives, Then the payment is not auto-applied and is surfaced as an exception 'invoice_already_paid'.
Configurable Priority: Ref Number then Email+Date Tolerance
Given the matching priority order is [invoice_id, reference_number, email_date_tolerance] and a settled payment lacks invoice_id but has reference_number matching an open invoice custom reference, When the engine runs, Then it maps using rule 'reference_number_exact' and records that rules were evaluated in priority order. Given a settled payment has no invoice_id or reference_number but payer email matches a client email and payment date within +/- 3 days of a scheduled session with an open invoice of equal amount, When the engine runs, Then it maps using rule 'email_date_amount' and records tolerance window applied. Given the priority configuration is changed to [email_date_tolerance, invoice_id, reference_number], When processing the same dataset, Then the engine selects the first satisfied rule per new priority and records the config version in the explanation.
Amount Small-Variance Threshold Handling
Given amount_tolerance is configured as max(50 cents, 0.5%) and a settled payment has amount $99.70 against an open invoice $100.00, When the engine runs, Then it maps the payment, marks invoice 'paid_with_small_variance', records variance $0.30 as adjustment 'small_variance', and includes tolerance details in explanation. Given a settled payment amount $100.60 against open invoice $100.00 with tolerance 50 cents, When the engine runs, Then it does not auto-match and surfaces exception 'amount_exceeds_tolerance'. Given tolerance changed to max(75 cents, 1%), When reprocessing the same payment, Then the payment matches and explanation cites updated tolerance and config version.
Duplicate Detection and Idempotency Across Imports
Given an import file contains a transaction with processor_id X that has already been matched, When the engine runs, Then it ignores the duplicate, creates no new allocations, updates no invoice states, and logs 'duplicate_transaction' in audit trail. Given two different files include the same processor_id X, When processed sequentially or concurrently, Then exactly one match allocation exists and the operation is idempotent and thread-safe. Given two distinct payments with same amount/date/email but different processor_id, When the engine runs, Then they are treated as separate and matched individually without false duplicate flags.
Authorization vs Settlement and Refund Mapping
Given a payment in 'authorized' state (not settled), When the engine runs, Then it does not post any allocation, marks the item as 'awaiting_settlement', and creates no invoice state changes. Given a subsequent 'settled' event for the same authorization with processor_id X and metadata linking to the same source, When the engine runs, Then it applies the payment to the intended invoice per rules and updates the prior pending record. Given a refund event referencing the original charge processor_id X for a previously matched invoice, When the engine runs, Then it maps the refund to the same invoice, updates invoice state to 'refunded' or 'partially_refunded' accordingly, and records a refund explanation including original match id.
Auto-Allocation Across Multiple Invoices (FIFO)
Given a client has two open invoices: A $60 (older) and B $40 (newer), and a settled payment of $100 arrives from that client, When the engine runs, Then it allocates $60 to invoice A and $40 to invoice B, marks both paid, and records allocation breakdown in explanation. Given a client has invoice A $120 open and receives a $70 settled payment, When the engine runs, Then it allocates $70 to invoice A, marks A 'partial', leaves $50 outstanding, and records partial allocation details. Given a client has all invoices paid and receives a $20 overpayment, When the engine runs, Then it does not create negative balances, surfaces exception 'unallocated_overpayment', and keeps funds unassigned pending manual action.
Ambiguous Matches Routed to Manual Selection with Audit
Given a settled payment matches two candidate invoices within date and amount tolerance, When the engine runs, Then it classifies the payment as 'ambiguous', does not auto-assign, and surfaces both candidates with scored rationale. Given a user manually selects invoice B in the UI and confirms, When the action is applied, Then the payment is deterministically assigned to invoice B, an explanation records 'manual_override' with who/when/why, and re-runs remain idempotent. Given no user action within 48 hours, When nightly sweep runs, Then the payment remains in exceptions with reminder generated; no auto assignment occurs until configuration explicitly enables it.
Exceptions Dashboard & Resolution Workflow
"As a coach managing billing, I want a clear list of exceptions with guided fixes so that I can quickly resolve blockers and finish reconciliation."
Description

Surface all reconciliation exceptions in a dedicated dashboard with clear categories (failed card, PO required, amount mismatch, missing invoice, ACH pending, chargeback/dispute). Provide inline resolution actions such as retry payment, send payment link, upload/attach PO, edit allocation, create/attach invoice, write off balance, or add an internal note. Include filters, sorting, bulk actions, and SLA indicators to prioritize critical items. Notify users of new exceptions and track resolution status to ensure the sweep can complete with minimal back-and-forth.

Acceptance Criteria
Categorized Exceptions Dashboard Load
Given a workspace with exceptions across failed card, PO required, amount mismatch, missing invoice, ACH pending, and chargeback/dispute categories When the user opens the Exceptions Dashboard for a sweep Then the dashboard groups exceptions by category and displays a count for each category And each exception row shows client name, service/session name, service date, invoice/transaction reference (if any), amount due, category, age in days, owner (if any), and SLA status badge And the initial view loads in ≤ 2 seconds for up to 2,000 exceptions; beyond that, pagination or infinite scroll is available And when there are 0 exceptions, an empty state with a “Return to Reconcile Sweep” action is displayed
Inline Resolution Actions by Exception Type
Given an exception of type failed card with a stored payment method When the user clicks Retry Payment Then the payment is attempted and, on success, the exception resolves to Paid, a receipt is sent to the client, and the invoice is marked paid; on failure, the exception remains open and displays the latest processor error code/message with a 60-second retry cooldown Given an exception of type failed card When the user clicks Send Payment Link Then a secure payment link is generated, displayed for copy, and emailed to the client; delivery and link creation are logged in the audit trail Given an exception of type PO required When the user uploads or attaches a PO (PDF/JPEG/PNG up to 10 MB) Then the file is validated, stored, linked to the exception, and the exception moves to In Progress; if the PO covers the amount, the user can mark as Resolved Given an exception of type amount mismatch When the user edits allocation/line items and saves Then validation prevents negative totals, logs the change, and if the difference is ≤ $0.01 tolerance the exception resolves; otherwise it remains open with the new variance shown Given an exception of type missing invoice When the user creates a new invoice from the exception or attaches an existing invoice Then the invoice links to the session and the exception resolves Given an exception of type ACH pending When the user clicks Refresh Status Then the processor is queried and status updates; upon settlement the invoice marks paid and the exception auto-resolves; if > 5 business days pending, the SLA status becomes Breached Given an exception of type chargeback/dispute When the user selects Mark as Disputed or Write Off Balance and provides a required reason Then the exception status updates accordingly, receipts are disabled, and the action is recorded for export
Filters, Sorting, and Bulk Actions
Given a list of exceptions on the dashboard When the user applies filters by category, client, owner, status (Open/In Progress/Resolved/Written Off), age range, or amount range Then only matching exceptions are displayed and the filter set persists for the user across sessions And the user can sort by age (default desc), amount, client name, or last updated And filter changes return results in ≤ 1 second for up to 2,000 items Given multiple selectable exceptions When the user selects items (including Select All across pages up to 500 items) Then the selection count is shown and only context-valid bulk actions are enabled (e.g., Retry Payment for failed card) When the user executes a bulk action (retry payment, send payment link, assign owner, write off) Then a confirmation appears and, after execution, a summary shows successes and failures with per-item error messages; partial completion does not cancel remaining items
SLA Indicators and Prioritization
Given workspace-level SLA thresholds per category (defaults: failed card due in 48h, PO required in 5 business days, ACH pending in 5 business days, chargeback response in 10 days) When exceptions are displayed Then each exception shows a due-by timestamp and an SLA badge: On Track (green), Due Soon (amber, within 24h), Breached (red, past due), with tooltip details; badges meet ≥ 4.5:1 contrast ratio And the default sort orders by SLA state (Breached, then Due Soon, then On Track) and within each by age descending And SLA badges update within 60 seconds of state changes without a full page reload And a summary header shows counts per SLA state and total outstanding
Notifications for New or Reopened Exceptions
Given notifications are enabled for the user When a new exception is created or an exception reopens in their workspace Then the user receives an in-app notification within 60 seconds with a deep link that opens the dashboard filtered to that exception And an email notification is sent within 15 minutes unless the user has opted out of email alerts And the same exception does not trigger duplicate notifications within a 24-hour window unless its category or status changes And marking an exception as Resolved clears its in-app unread indicator
Resolution Status Tracking, Audit Trail, and Sweep Completion Gate
Given an exception is updated When its status changes to Open, In Progress, Resolved, or Written Off Then the transition is validated (reason required for Written Off), actor and timestamp are recorded, and the change appears in the exception’s timeline along with any internal notes And internal notes are time-stamped, include author, are not client-visible, and are editable for 15 minutes with edit history retained Given all exceptions in the sweep are either Resolved or Written Off When the dashboard refreshes Then the Reconcile Sweep shows 0 exceptions remaining and enables the Complete Sweep action And the user can export a CSV of exceptions including fields: exception_id, category, created_at, current_status, status_changes, owner, resolution_action, resolved_at
Accounting Export & Data Mapping
"As an independent consultant, I want reconciled records to flow into my accounting tool with correct mappings so that my books stay accurate without manual data entry."
Description

Enable export of reconciled transactions to accounting systems (e.g., QuickBooks Online, Xero) and CSV with configurable field mapping (customers, items/services, tax codes, classes, chart of accounts). Support export of payments, partials, refunds, and write-offs as appropriate journal entries or payment records, with batching, scheduling, and webhooks. Ensure de-duplication on re-exports, include multi-currency amounts and exchange rates where applicable, and write back export status to SoloPilot for complete visibility.

Acceptance Criteria
QBO Export with Configurable Field Mapping
Given an authorized QuickBooks Online connection and completed mappings for customers, items/services, tax codes, classes, and accounts And a set of 1–1,000 reconciled transactions including payments, partials, refunds, and write-offs exists When the user initiates Export to QuickBooks Online from Reconcile Sweep Then the system creates appropriate accounting records per type using the configured mappings And each exported record’s amounts, dates, and tax calculations match the source transactions within ±0.01 of the transaction currency And line/class/tax code assignments reflect the mapping on each line or header as configured And the export finishes and returns a summary with counts by type (created, updated, skipped, failed)
De-duplication and Safe Re-Export
Given transactions were previously exported and each has a stored external_reference and last_exported_at in SoloPilot And no changes have been made since last export When the user re-exports the same date range or cohort Then no duplicate accounting records are created in the target system And the export summary reports 0 created and all as skipped:duplicate Given some previously exported transactions have changed (e.g., amount adjusted, refund added) When re-export is initiated Then the system updates the corresponding accounting records or creates supplemental records as per mapping, without creating duplicates And each export operation uses an idempotency key so retrying the same job does not change results And the summary reports counts for updated vs created with references to affected IDs
Multi-Currency Amounts and Exchange Rates
Given the organization base currency is USD and a reconciled transaction is in EUR with a stored exchange rate effective on the transaction date When the transaction is exported Then the target accounting record is created in EUR if supported by the target; otherwise, in USD using the stored exchange rate And the exported payload includes currency, exchange_rate, source_amount, and base_amount And rounding differences are ≤ 0.01 in base currency per transaction And the summary reports currency breakdown by ISO code
Scheduled Batch Exports
Given a daily export schedule at 02:00 local time is configured with a batch size limit of 500 records And there are eligible reconciled transactions not yet exported When the schedule triggers Then the system queues and runs an export job without user interaction And only transactions with export_status = Pending are included up to the batch limit And transient failures are retried up to 3 times with exponential backoff And upon completion, a notification is sent to the account owner with job status and counts
Webhook Notifications for Export Events
Given a webhook endpoint URL and shared secret are configured and verified When an export job transitions to started, succeeded, failed, or partial_success Then the system sends an HTTP POST with a JSON body containing job_id, status, counts, started_at, completed_at, and an errors array (if any) And each request includes an HMAC-SHA256 signature header computed with the shared secret And non-2xx responses trigger retries for up to 24 hours with exponential backoff And all delivery attempts are logged with timestamp and response code
Export Status Write-Back and Exceptions Dashboard
Given an export job completes When viewing the related transactions in SoloPilot Then each transaction shows export_status (Pending, Exported, Failed, Updated), external_reference, last_exported_at, and last_export_job_id And failures include a machine-readable error_code and human-readable message And the Reconcile Sweep dashboard surfaces an Exceptions view filterable by error_code and retriable in bulk
CSV Export with Mapping Parity
Given a user selects Export to CSV with a chosen field mapping When the export completes Then the CSV contains columns for transaction_id, contact_name, item, tax_code, class, account, type (payment|partial|refund|write_off), amount, currency, exchange_rate, base_amount, invoice_number, payment_date, and memo And the file is UTF-8 encoded, comma-delimited, RFC 4180 compliant, and accurately represents values within ±0.01 of source amounts And exports of up to 10,000 rows complete and are downloadable within 2 minutes under normal load And the download link is secured, expiring after 7 days
Automated Receipt Generation & Delivery
"As a therapist, I want receipts to be automatically sent after reconciliation so that clients have timely proof of payment without me doing extra work."
Description

Automatically generate branded receipts (PDF and email) for all reconciled payments, including line items, taxes, payment method details, and unique receipt numbers. Support per-session and consolidated receipts, batch sending after a sweep, resend on demand, and suppression for clients who opt out. Track delivery status and bounces, and archive receipts within the client timeline for reference. Templates use SoloPilot branding settings to ensure a professional and consistent client experience.

Acceptance Criteria
Batch Receipts After Reconcile Sweep
Given a completed Reconcile Sweep with up to 500 reconciled payments, When the user clicks "Send Receipts", Then receipts are generated for all eligible payments within 60 seconds and emails are queued. Given payments marked Paid, Partial, or Refunded in the sweep, When receipts are generated, Then each receipt reflects the correct status and corresponding amounts. Given generation succeeds, When delivery is initiated, Then a PDF receipt is attached and an HTML email body is included for each message. Given generation completes, Then each receipt is archived to the corresponding client timeline with a sweep reference ID within 10 seconds. Given any receipt fails to generate or queue, Then the error is logged, surfaced on the sweep dashboard with the receipt/payment reference, and other receipts continue processing.
Per-Session Receipt Generation
Given a reconciled payment linked to a single session, When a receipt is generated, Then the receipt shows the session date/time, service name, duration, and rate as line items. Given taxes are configured for the service, When the receipt is generated, Then taxes are calculated per configuration and shown with tax name and rate. Given a partial payment, When the receipt is generated, Then the receipt displays amount paid, remaining balance, and payment status "Partial". Given a refund for the session, When the receipt is generated, Then the receipt includes a Refund entry with date, amount, and resulting net total.
Consolidated Receipt Generation
Given a client has multiple reconciled payments selected for consolidation in a sweep, When a consolidated receipt is generated, Then all sessions within scope appear as individual line items on one receipt. Given multiple tax types apply across sessions, When the consolidated receipt is generated, Then a tax breakdown per tax type and a consolidated tax total are shown. Given adjustments or refunds exist across included sessions, When the consolidated receipt is generated, Then they are itemized and the net total equals sum(line items + taxes + adjustments + refunds). Given consolidation, When the receipt is generated, Then the file name and email subject include the consolidation period or sweep name and the unique receipt number.
Receipt Content, Format, and Branding
Given SoloPilot branding settings (logo, brand name, colors, business address, tax ID), When a receipt is generated, Then the PDF and email use these settings consistently. Then every receipt includes: unique receipt number, client name, provider name, issue date, payment method type and last 4 (e.g., Visa ••••1234, ACH), and auth/reference ID when available. Then every receipt includes: line items, unit price, quantity, subtotal, taxes, discounts/adjustments, total, amount paid, and balance (if any). Given duplicate generation attempts for the same payment/consolidation, Then the same receipt number is reused and remains unique within the workspace.
Delivery Status Tracking and Bounce Handling
Given a receipt email is queued, Then delivery status transitions are tracked with timestamps: Queued -> Sent -> Delivered or Bounced. Given an SMTP/ESP bounce occurs, Then the bounce reason/code are captured, status is set to Bounced, and the sweep dashboard surfaces the exception with client and receipt number. Given a temporary delivery failure, Then the system retries up to 3 times over 15 minutes and records each attempt; after retries, status is Bounced. Given link tracking is enabled, When the client opens the emailed receipt link, Then a download/open audit record is stored with timestamp.
Resend Receipt On Demand
Given a previously generated receipt, When a user clicks Resend from the client timeline or sweep dashboard, Then the same receipt number is used and a new delivery attempt is recorded. Given the user edits the recipient email before resending, When Resend is confirmed, Then delivery is sent to the new address and the audit log records the updated recipient. Given a receipt status is Bounced, When the address is corrected and Resend is triggered, Then the new attempt updates status accordingly and the prior bounce remains in history. Given suppression rules do not apply to this client, When Resend is used, Then no re-generation is required and the original PDF is reused.
Opt-Out Suppression and Audit
Given a client has opted out of receipt emails, When a sweep generates receipts, Then the receipt is generated and archived but no email is sent. Then the sweep dashboard lists suppressed deliveries with reason "Opted Out" and provides a one-time override control requiring explicit confirmation. Given an override is used, When the receipt is sent, Then the action is audited with user, timestamp, and reason, and suppression remains active for future sweeps. Given suppression is active, Then no outbound email/webhook is dispatched for that receipt.
Audit Trail, Permissions & Undo
"As a business owner, I want a secure audit log and controlled access so that I can ensure compliance and recover from mistakes without data loss."
Description

Record an immutable audit log of all reconciliation actions (who, when, what changed, before/after values, reason) with export capability. Enforce role-based permissions (e.g., Owner, Finance, Practitioner) to control who can run sweeps, resolve exceptions, export accounting data, or issue refunds. Provide a time-bound undo for bulk operations to safely revert accidental changes, while preserving a complete history for compliance and trust.

Acceptance Criteria
Immutable Audit Log for Reconcile Sweep
Given a user with permission initiates a Reconcile Sweep that updates N records When the sweep completes Then exactly N audit entries are appended, one per affected record, all linked by the same sweep_id And each entry is durable and queryable within 2 seconds of completion And any attempt to modify or delete an audit entry via UI or API returns 403 and creates a separate audit entry of type access_denied Given the system restarts When the service comes back online Then previously written audit entries remain intact and readable
Audit Entry Field Completeness and Formatting
Given any reconciliation action occurs (mark_paid, mark_partial, mark_refunded, issue_receipt, resolve_exception, export_triggered, export_completed, undo) When the audit entry is written Then it includes actor_user_id, actor_role, action_type, entity_type, entity_id, sweep_id (nullable), request_id, timestamp_utc (ISO 8601), ip_address, source (UI/API), before_values, after_values, reason (required for refund/undo/write_off), and outcome (success/failure) And fields not applicable are explicitly null and not omitted And before_values and after_values capture only changed attributes with previous and new values Given multiple entries share a sweep_id When queried ordered by timestamp_utc Then their timestamps are non-decreasing
Filtered Audit Export to Accounting
Given a Finance or Owner user requests an audit export with filters (date_range, action_types, roles, sweep_id, entity_types, outcome) When the request is submitted Then the system generates both CSV and JSON exports with a documented header set matching the audited fields And the export is delivered via a signed URL that expires in 24 hours And PII fields (client_email, client_phone) are redacted for Practitioner role exports Given the result set exceeds 100,000 rows When the export is requested Then the job runs asynchronously, exposes progress (0–100%), and completes within 15 minutes for up to 5,000,000 rows And the completed export includes a SHA-256 checksum and row_count metadata Given an export is generated When the export is downloaded Then the download is logged with actor_user_id, ip_address, and timestamp_utc
Role-Based Permissions Matrix Enforcement
Rule: Owner can perform run_sweep, resolve_exception, export_accounting, issue_refund, view_audit, undo_sweep Rule: Finance can perform run_sweep, resolve_exception, export_accounting, issue_refund, view_audit, undo_sweep Rule: Practitioner can view_audit for assigned records and resolve_exception for assigned records; cannot run_sweep, export_accounting, issue_refund, or undo_sweep Given a user attempts a disallowed action When the action is invoked via UI or API Then the UI hides or disables controls, and the API returns 403 with code permission_denied and an error_id And the denial is recorded in the audit log with action_type access_denied and includes attempted_action Given a user's role changes When the change is saved Then the change is audited and takes effect on the next request without requiring a service restart
Unauthorized Actions Are Blocked and Audited
Given a signed-in user without refund permission attempts to issue a refund When the request is made Then no financial state changes occur And API returns 403 with code permission_denied within 300 ms p95 And an audit entry is recorded with outcome failure and reason permission_denied Given an unauthenticated request is made to audit endpoints When the request is processed Then the system returns 401 with no data leakage and rate-limits repeated attempts (HTTP 429 after 10 failed attempts per minute per IP) And each attempt is logged with ip_address and user_agent (if present)
Time-Bound Undo for Bulk Reconcile Operations
Given an Owner or Finance user completes a Reconcile Sweep When they view the operation within 15 minutes Then an Undo option is available for the entire sweep_id Given Undo is triggered When no affected record has been modified since the sweep Then all changes made by the sweep are reverted in a single transaction And new audit entries are appended for each reverted record with action_type undo and a required reason And the original audit entries remain unchanged and linked to the undo via correlation_id Given Undo is triggered When some records have subsequent changes after the sweep Then only unchanged records are reverted And a conflict report lists non-reverted records with reasons conflict_detected And no partial revert leaves a record in an invalid state Given the 15-minute window has expired When the user attempts to undo Then the system disables the action and displays a message explaining the policy
Receipts and Refunds Are Traceable and Notified
Given a payment is marked as paid or partial during a sweep When receipts are issued Then a receipt PDF is generated per invoice with a unique audit_reference_id and sent to the client's email on file And delivery success or failure is logged in the audit with outcome success or failure Given a refund is issued When the user submits the refund Then a reason is required, amount is less than or equal to the original paid amount, currency matches the invoice currency, and partial refunds are supported And the refund event is audited with before_values and after_values reflecting balance and status And the accounting export reflects the refund in the next run with an idempotency key preventing duplicates
Partial Payments, Refunds & Adjustments Handling
"As a freelancer, I want the system to handle partials, credits, and refunds correctly so that balances are always accurate and I don’t have to reconcile edge cases manually."
Description

Support partial payments, split allocations across multiple invoices, overpayments resulting in credits, and full/partial refunds that correctly update balances and statuses. Allow write-offs and discounts as explicit adjustments with reasons, and ensure all adjustments flow through exports, receipts, and the audit log. Maintain accurate running balances at the client, invoice, and session levels to keep the ledger consistent across SoloPilot and external accounting systems.

Acceptance Criteria
Partial Payment on Multi-Session Invoice
- Given an open invoice with 3 session line items totaling $600 (3×$200) and status "Unpaid" When a $250 card payment is recorded via Reconcile Sweep for that invoice Then the invoice status updates to "Partially Paid" And $200 is allocated to Session A and $50 to Session B (oldest-first rule) And remaining balances are Session B $150 and Session C $200; invoice remaining balance is $350 And client-level balance decreases by $250; all amounts rounded to 2 decimals And a receipt is generated showing amount paid, remaining balance, and itemized allocation And an audit log entry captures payer, method, amount, allocation map (session IDs→amounts), timestamp, and actor And the payment and updated balances appear in the accounting export with invoice and session references
Split Payment Across Multiple Invoices
- Given a client with two open invoices (#101: $300, #102: $200) When a single $400 ACH payment is recorded and allocated $250 to #101 and $150 to #102 via Reconcile Sweep Then #101 status becomes "Partially Paid" (balance $50) and #102 status becomes "Partially Paid" (balance $50) And one payment record exists with allocation lines referencing invoice IDs and amounts And the client balance decreases by $400; amounts rounded to 2 decimals And exports include one payment entry with two allocation lines; receipts clearly show split allocation And the audit log records payment reference, allocation breakdown, actor, and timestamp And re-submitting the same payment reference is rejected as a duplicate unless an explicit override flag is provided
Overpayment Creates Client Credit
- Given an open invoice #205 with balance $180 When a $200 payment is recorded Then $180 is applied to #205 and its status becomes "Paid" And a $20 client credit memo is created and available for application And client balance decreases by $200 and credit balance increases by $20 And the credit auto-applies FIFO to the next open invoice unless auto-apply is disabled in settings And exports include a $200 payment, $180 application to #205, and a $20 credit memo entry And receipts show the overpayment and resulting credit And the audit log captures credit creation and any subsequent auto-application with references
Partial and Full Refund Processing
- Given an invoice previously paid $300 via payment P-123 When a partial refund of $80 is issued to the original method Then a refund record R-1 is created linked to P-123 and the invoice And the invoice status is "Partially Refunded" and refundable remaining amount is tracked And balances update: client balance increases by $80 if invoice remains open; otherwise a $80 credit memo is created And exports include a negative refund line for $80 with references to P-123 and the invoice And a refund receipt is issued; audit log stores reason code, actor, timestamp, and references And total refunded across all refunds cannot exceed total paid; attempts beyond the limit are blocked with a clear error - Given a full refund is issued for all payments on a closed invoice Then the invoice status changes to "Refunded" and balances reverse appropriately And any previously auto-applied credits from the refunded payment are reversed in correct order and logged
Adjustments: Discounts and Write-Offs with Reasons
- Given a user with "Adjust Invoices" permission and an open invoice with $200 balance When they apply a $30 discount with reason "Loyalty" Then an adjustment record is created and invoice balance becomes $170 without cash movement And the adjustment appears as a distinct line on the invoice and receipt with reason and actor And the adjustment is included in exports with type "Discount", amount, reason, actor, and timestamp And the audit log captures before/after amounts, reason, actor, and timestamp And adjustments can be reverted via a reversal entry that restores balances and is logged - Given a write-off of $50 is applied to an uncollectible balance Then client balance decreases by $50 And invoice status remains "Partially Paid" if residual > $0 else becomes "Written Off" And exports map the write-off to "Bad Debt" (or configured account) with references
Running Balances and Export Consistency Across Levels
- Given multiple payments, refunds, credits, and adjustments over time for a client When Reconcile Sweep completes Then client-level balance equals the sum of open invoice balances minus client credits within a $0.01 tolerance And each invoice balance equals sum(line items + adjustments - payments - refunds - write-offs), rounded to 2 decimals And each session line item shows applied amounts and remaining balance consistent with its invoice total And accounting export totals for payments, refunds, discounts, and write-offs match SoloPilot ledger totals for the same period And a validation report flags any discrepancy and blocks export if discrepancy > $0.01 And per-entity last reconciled timestamps are updated And every change has an immutable audit log entry with entity IDs and version numbers

Straggler Nudges

Automated, polite follow‑ups to unpaid attendees with refreshed paylinks and mini‑statements. Escalates channels (email→SMS→DM) at smart intervals, supports payment plans when allowed, and pauses if a dispute is opened—recovering revenue without uncomfortable back‑and‑forth.

Requirements

Smart Escalation Engine
"As an independent practitioner, I want unpaid clients to receive smart, escalating reminders across channels so that I recover revenue without spending time chasing or risking awkward follow-ups."
Description

Implements configurable, multi-channel follow-up sequences that progress from email to SMS to DM for unpaid sessions and invoices. Triggers from SoloPilot’s billing state changes and attendance records, respects client channel preferences, local time zones, quiet hours, and holiday calendars, and stops automatically on payment, manual resolution, opt-out, or bounce. Supports per-workspace policies (max touches, interval timing, escalation rules), dynamic personalization (name, session details), idempotent scheduling, and audit logging. Integrates with invoicing, contact profiles, and automation scheduler to ensure timely, polite nudges without manual intervention.

Acceptance Criteria
Auto-Trigger on Unpaid Session and Invoice States
Given a session is marked Attended with a billable rate and no linked payment, When the session record is saved, Then a new nudge sequence is scheduled per the active workspace policy. Given an invoice transitions to Overdue with balance > 0, When the billing state change is persisted, Then the first nudge is scheduled within the policy-defined initial delay. Given a payment plan installment transitions to Overdue, When the state change event is received, Then a nudge sequence is scheduled referencing the installment and parent invoice. Given the invoice or session balance is 0 or marked Comped, When a state change occurs, Then no nudge sequence is created. Given the contact or invoice is flagged Do Not Follow Up or the feature flag is disabled at the workspace, When an otherwise-eligible trigger occurs, Then no nudge sequence is created. Given a sequence is scheduled and the invoice becomes Paid before the first send, When the scheduler evaluates the queue, Then the sequence is canceled with reason Paid and no message is sent.
Escalation Sequence Respects Preferences and Workspace Policies
Given the workspace policy defines channel order [Email → SMS → DM], max_touches N, and intervals between steps, When a sequence is created, Then steps are scheduled in that order, spacing, and count. Given a contact has Email allowed but SMS opted out and no DM handle, When escalation occurs after email, Then the SMS and DM steps are skipped and the sequence ends when max_touches is reached. Given a channel lacks required contact data (e.g., no phone number), When scheduling that step, Then the step is skipped and the next allowed channel is scheduled. Given an email hard bounces on step k and max_touches is not reached, When the bounce is recorded, Then the next allowed channel step k+1 is scheduled per interval and the email channel is disabled for the sequence. Given max_touches has been reached and the invoice remains unpaid, When the next step would schedule, Then no further messages are scheduled and the sequence ends with reason MaxTouchesReached. Given a contact preference is Email-only, When the sequence is created, Then only email steps are included regardless of workspace default channel order.
Quiet Hours, Local Time Zone, and Holiday Compliance
Given a contact timezone is set to T, When calculating send times, Then all steps are scheduled in timezone T. Given a calculated send time falls within workspace quiet hours for T, When scheduling, Then the send time is moved to the next allowed window start within T. Given the planned send date is a holiday per the workspace holiday calendar for the contact locale, When scheduling, Then the step is moved to the next non-holiday business day at the policy default send time. Given no contact timezone is set, When scheduling, Then the workspace timezone is used. Given the workspace policy excludes weekends, When a calculated send falls on Saturday or Sunday, Then the send is moved to the next business day at the policy default send time.
Automatic Stop, Pause, and Opt-out Handling
Given an invoice linked to an active sequence is paid in full, When the payment is posted, Then all pending steps are canceled within 60 seconds and no further messages are sent. Given a user marks an invoice as Written Off or Manually Resolved, When the status change is saved, Then all pending steps are canceled with reason ManualResolution. Given a recipient replies STOP to an SMS, When the opt-out is processed, Then SMS steps are canceled for this contact across the workspace and the sequence continues only via remaining allowed channels; if none remain, the sequence ends with reason OptOut. Given a recipient clicks an email unsubscribe link, When the unsubscribe is recorded, Then no further emails are sent to this contact from any sequence and the current sequence continues only via remaining allowed channels. Given an email hard bounce occurs, When the bounce is recorded, Then future email steps are canceled and the next allowed channel is scheduled per policy. Given a dispute is opened on the invoice, When the dispute event is received, Then all pending steps are paused; no messages are sent until the dispute is closed; upon dispute closure, if balance > 0 and not opted out, Then the sequence resumes from the next step with schedules recomputed respecting quiet hours and holidays.
Dynamic Personalization and Paylink Refresh
Given a message template includes tokens for first_name, session_date, amount_due, and invoice_link, When a step is sent, Then the rendered message contains resolved values with fallbacks (e.g., first_name → "there") and no unresolved placeholders. Given the invoice has multiple unpaid items, When rendering the mini-statement, Then the message includes the total outstanding and up to the first 3 line items with amounts. Given the invoice supports payment plans and the workspace allows offering plans, When composing the message, Then the content includes a payment plan offer CTA and link; otherwise, the plan offer is omitted. Given a refreshed paylink is generated at send time, When the recipient opens it, Then it loads a payment page for the correct invoice with the current outstanding balance and accepts payment. Given the recipient pays via the refreshed paylink, When payment is confirmed, Then the sequence is stopped and any pending steps are canceled within 60 seconds.
Idempotent Scheduling Across Retries and Deploys
Given scheduler retries the same job due to a transient error, When the job is re-run, Then no duplicate step is created or sent for the same (workspace, contact, invoice, sequence_step, send_at) key. Given concurrent triggers occur from both attendance and billing events for the same invoice, When evaluating sequence creation, Then only a single sequence is created for that invoice-contact pair. Given a step send operation times out and is retried by the worker, When the provider receives the request, Then at most one message is delivered and duplicates are suppressed by provider message idempotency key. Given a step is rescheduled (e.g., due to quiet hours or holiday), When persisting the change, Then the original pending task is canceled and replaced; only the latest schedule remains active. Given the application restarts or a new deployment occurs, When the scheduler resumes, Then previously scheduled steps remain intact without duplication or loss.
Audit Logging Completeness and Integrity
Given a sequence is created or a state transition occurs (send, skip, pause, cancel), When the event happens, Then an audit log entry is written with timestamp (UTC), workspace_id, contact_id, invoice_id, channel, template_id (if applicable), actor (system/user), reason code, and previous→new state. Given a message is queued/sent/bounced/delivered, When provider callbacks are processed, Then corresponding audit log entries are appended with provider message_id and delivery status. Given audit logs are stored, When a user attempts to edit an existing entry, Then the system prevents modification and any correction is recorded as a new append-only entry. Given a user views the invoice or contact timeline, When querying logs, Then events for that entity are returned in reverse chronological order within 2 seconds for the last 100 events. Given a user exports audit logs for a date range and workspace, When the export is generated, Then the file contains a complete, chronologically ordered set of entries matching the filters.
Refreshable Paylinks
"As a client receiving a reminder, I want a one-click, secure link to pay my exact balance so that I can settle quickly without confusion or extra steps."
Description

Generates secure, short-lived, trackable paylinks that auto-populate the correct invoice, outstanding balance, and permitted payment options. Automatically refreshes links per nudge, supports partial and full payments, and deep-links to the connected processor’s checkout with line items and taxes prefilled. Handles expiration, token rotation, and fraud safeguards; embeds attribution parameters for analytics; and renders channel-appropriate formats (short URLs for SMS/DM, full URLs/buttons for email). Integrates with SoloPilot invoices, payment processors, and the escalation engine.

Acceptance Criteria
Paylink Generates With Correct Invoice and Balance
Given an unpaid SoloPilot invoice with an outstanding balance and defined payment options exists When a Straggler Nudge triggers paylink creation for the invoice’s recipient Then a unique HTTPS paylink is generated and resolves to the correct invoice ID And the displayed balance equals the invoice’s outstanding amount at generation time And only the invoice-permitted payment methods (e.g., full, partial, plan) are available And the link has a configurable TTL between 24h and 7d (default 72h) stored with the link record And the link includes a non-PII signed token and a click_id for tracking
Auto-Refresh Per Nudge and Invalidate Prior Links
Given an existing paylink for a specific invoice and recipient When the escalation engine sends a new nudge for the same invoice Then a new paylink is generated with refreshed TTL and the current outstanding balance And all prior unredeemed links for that invoice+recipient immediately return HTTP 410 with an "expired link" message And prior tokens are rotated and cannot be reactivated And refresh is skipped (no new link) if the invoice/contact is paused or a dispute flag is present And an audit record is stored with old_link_id, new_link_id, timestamp, nudge_id, and channel
Deep-Link Checkout With Prefilled Items, Taxes, and Partial/Full Payments
Given a recipient clicks a valid paylink When redirected to the connected payment processor checkout Then the checkout shows prefilled line items and taxes exactly matching the SoloPilot invoice And currency and amount reflect either the full balance or a user-entered partial amount if permitted And payment plan options appear only when allowed and selection updates schedule and amounts accordingly And the user cannot add/remove items, change tax, or modify currency in checkout And on successful payment, a webhook updates the invoice balance within 60 seconds and marks the link redeemed
Channel-Appropriate Link Rendering
Given a nudge is sent by email When the message is rendered Then it includes a CTA button and a readable full URL fallback, with a plain-text part containing the full URL And email link scanning/click-tracking does not break token verification Given a nudge is sent by SMS or DM When the message is rendered Then a branded short URL of 30 characters or fewer is used And the message remains under 320 characters including the short URL And link previews are disabled where supported to avoid token leakage
Expiration Handling and Self-Service Refresh
Given a paylink has passed its TTL When the link is clicked Then an expiry page is shown with invoice summary and a one-click "Send me a fresh link" action And requesting a fresh link triggers a new nudge via the last successful channel and permanently invalidates the expired token And expiration and refresh attempts are logged with link_id, invoice_id, user agent, and IP And any tampered or malformed token returns HTTP 401 without revealing invoice existence
Security and Fraud Safeguards
Given a valid paylink When any URL parameter (e.g., amount, invoice_id, token) is modified Then access is denied with HTTP 401 and the attempt is rate-limited And after 5 invalid attempts from the same IP/user agent within 10 minutes, further requests return HTTP 429 for 30 minutes And all paylink endpoints enforce HTTPS, HSTS, and no-cache headers And tokens have ≥128 bits of entropy, are server-signed, and contain no PII; links are scoped to a single invoice And security events (invalid, rate-limited, expired) are emitted to the audit log
Analytics and Attribution Across Refreshes
Given a paylink is generated or refreshed When it is sent via a channel and later clicked or converted Then UTM and internal params (utm_source, utm_medium, utm_campaign, nudge_id, channel, link_id, click_id) are appended and preserved through redirects And click and conversion events are deduplicated per session and associated with the correct invoice, contact, nudge, and channel And attribution metadata is forwarded to the processor checkout metadata when supported And analytics events are queryable in SoloPilot within 5 minutes of occurrence
Mini-Statement Generator
"As a client, I want a clear mini-statement with each reminder so that I understand exactly what I owe and why before I pay."
Description

Creates concise, channel-optimized summaries of what’s owed to accompany each nudge, including invoice number(s), service name, amount due, due date, last payment, and payment plan status when applicable. Adapts formatting to each channel’s character limits, supports multi-currency and tax display, and attaches a PDF statement for email while providing compact text for SMS/DM. Pulls real-time data from invoicing and sessions, localizes currency/date formats, and ensures consistency with the ledger.

Acceptance Criteria
Email Mini-Statement With PDF Attachment
Given an overdue balance exists and Email is the selected channel When the mini-statement is generated Then the email body contains: invoice number(s), service name(s), total amount due, due date, last payment amount and date, and payment plan status if applicable And a refreshed paylink is included as both a button and plaintext URL And a PDF statement is attached named "Statement-<ClientName>-<YYYYMMDD>.pdf" with size <= 500 KB And PDF contents match the email body fields and the ledger totals within 0.01 of the currency unit And tax rate and tax amount are displayed when tax applies
SMS Compact Statement Under Character Limits
Given SMS is the selected channel and an unpaid balance exists When the mini-statement is generated Then the message length is <= 160 characters including the paylink And the message includes: total due, earliest due date, primary invoice number, currency symbol or ISO code, and a shortened refreshed paylink And no line breaks are present And if the initial composition would exceed 160 characters, abbreviations and elisions are applied to meet the limit without removing the amount due or paylink
DM/Chat Statement Formatting
Given a DM/Chat channel (e.g., WhatsApp, Slack) with a 500-character limit is the selected channel When the mini-statement is generated Then the message length is <= 500 characters And the message includes: total due, oldest due date, up to 3 invoice numbers, last payment amount and date if present, payment plan status if applicable, currency and tax indicator if tax applies, and a refreshed paylink And only formatting supported by the channel is used; unsupported markup is stripped
Multi-Invoice Aggregation And Summarization
Given a client has multiple unpaid invoices When the mini-statement is generated Then the total amount due equals the sum of all open invoice balances and matches the ledger within 0.01 And Email lists all invoice numbers; DM lists up to 3; SMS lists 1; remaining invoices are summarized as "+N more" And service names are included for Email and DM; omitted for SMS if needed to meet limits And the oldest due date is displayed as the aggregate due date
Localization Of Currency, Tax, And Dates
Given the client locale, client time zone, workspace locale preferences, and invoice currency are available When the mini-statement is generated Then currency and dates are formatted per client locale and workspace preferences; time zone uses client time zone or falls back to workspace default And ambiguous currency symbols append the ISO code (e.g., CA$) And taxes are displayed as inclusive or exclusive per tax settings, showing rate and amount when tax applies And amounts are rounded using the currency’s minor units and match ledger totals within 0.01 And this is validated for en-US (USD), fr-FR (EUR), en-GB (GBP), and ja-JP (JPY)
Real-Time Ledger Consistency And Freshness
Given the ledger can change due to incoming payments When the mini-statement is generated for sending Then data is fetched from invoicing and sessions in real time within 1 second of send And if any relevant balance or last payment changes before send, the statement is regenerated; if a mismatch > 0.01 persists, sending is aborted and retried later And the last payment amount and date reflect the most recent settled transaction at send time And end-to-end generation completes in <= 3 seconds at the 95th percentile
Payment Plan Status Rendering
Given an invoice is on a payment plan When the mini-statement is generated Then it displays: "Installment X/Y", next due date, current installment amount, and remaining balance And if on-time, status shows "On track"; if past-due, shows "Overdue by N days" And for SMS, the plan status is condensed to "Plan X/Y, Next: YYYY-MM-DD" to fit limits And if no payment plan exists, no plan status text is shown
Payment Plan Offer Rules
"As a coach, I want eligible clients to be offered installment plans in reminders so that more invoices get paid without me negotiating terms manually."
Description

Enables conditional inclusion of installment options within nudges when the workspace allows payment plans. Defines eligibility criteria (invoice minimums, client history, services), plan terms (number of installments, schedule, fees), and acceptance flow via paylink. On acceptance, auto-generates the installment schedule, updates the invoice, and adjusts future nudges to reference the plan and remaining balance. Includes safeguards for delinquency (grace periods, re-nudge cadence) and full auditability.

Acceptance Criteria
Eligibility Evaluation for Payment Plan Offer in Nudge
Given workspace.paymentPlans.enabled = true AND invoice.status = 'Unpaid' And invoice.total >= workspace.paymentPlans.minInvoiceAmount And (workspace.paymentPlans.eligibleServiceIds is empty OR invoice.serviceId ∈ workspace.paymentPlans.eligibleServiceIds) And client.failedPaymentsLast90d <= workspace.paymentPlans.maxFailedPayments And client.hasActiveDefaultedPlan = false When a Straggler Nudge is generated for the invoice Then the nudge renders a "Pay in installments" offer with >= 1 plan option from workspace.paymentPlans.allowedPlans And the eligibility decision (pass/fail), evaluated fields, and reasonCode are written to the audit log within 1 second of nudge creation Given any eligibility check fails When the nudge is generated Then no installment offer is shown And reasonCode is logged and visible in the nudge preview inspector
Plan Terms and Paylink Generation
Given eligibility = true When composing the nudge Then generate a unique paylink with TTL = workspace.links.ttlHours (default 72) And the paylink page displays allowed plan options: numberOfInstallments within workspace.paymentPlans.installmentsRange, schedule computed with firstChargeTiming ∈ {"immediate","nextBusinessDay"}, subsequent charges monthly aligned to day-of-month, weekends/holidays shifted to next business day per workspace.calendar And all applicable fees (setupFee, perInstallmentFee) are disclosed; totalCost = invoice.total + applicable fees is calculated and shown And currency/locale/tax match the invoice And the paylink is single-use; visiting after expiry returns 410; a new link is generated on the next nudge
Acceptance Flow via Paylink
Given the recipient opens the paylink and selects an allowed plan And they affirm terms via required consent checkbox; 3DS/SCA is completed when required by the processor/region When they submit acceptance Then the system creates InstallmentSchedule with N installments summing to totalCost; rounding uses last-installment-adjust rule; each installment amount is rounded to 2 decimals And invoice.status is updated to 'On Plan' and remainingBalance = totalCost - firstPaymentCaptured And first installment is authorized/charged per workspace.paymentPlans.firstChargeTiming; on failure, invoice remains 'Unpaid' and no plan is created And confirmation is sent via the nudge channel within 1 minute; receipt lists schedule and next due date And an immutable audit record stores termsSnapshotHash, client IP, userAgent, and timestamp
Post-Acceptance Nudge Adjustments
Given invoice.status = 'On Plan' AND plan.status = 'Active' When Straggler Nudge cadence runs Then the system uses the 'on-plan' template And the message includes remainingBalance, nextDueDate, pastDueDays (if > 0), and a paylink to pay next installment or pay in full And the original 'pay in full only' CTA is hidden unless workspace.paymentPlans.allowEarlyPayoff = true And escalation channels (email -> SMS -> DM) and cooldowns follow workspace.nudges.cadence while respecting paused states And amounts/dates reflect real-time values at send time
Delinquency Grace Period and Escalation
Given an installment dueDate passes without successful capture When within workspace.paymentPlans.gracePeriodDays Then installment.status = 'Grace' and send reminder cadence per workspace.nudges.graceCadence When grace period ends and payment is still not captured Then installment.status = 'Past Due' and apply lateFee if enabled (capped by workspace.paymentPlans.maxLateFee) And trigger retries per workspace.payments.retryPolicy (maxRetries, backoff) and escalate channels with defined cooldowns When defaultThreshold (daysPastDue or failedAttempts) is exceeded Then plan.status = 'Defaulted', remaining balance converts to due-in-full, invoice.status = 'Unpaid', and future nudges switch to 'due-in-full' messaging
Pause on Dispute or Open Issue
Given a dispute/chargeback or internal support ticket is opened for the invoice or any installment When the event is received Then communicationsPaused = true for the plan/invoice; cancel scheduled retries and suppress all nudges And the UI shows 'Paused — Dispute Open' with link to details When dispute outcome = 'Won' Then resume nudges and retries with dates recalculated from the resume timestamp When dispute outcome = 'Lost' Then cancel the plan, reverse future installments, and adjust invoice per workspace.policies.disputeResolution And all state changes are recorded in the audit log with reason and actor
Audit Log Completeness and Export
Given a user with permission 'ExportAuditLogs' When requesting audit export for a client or invoice with a date range Then the system returns CSV and JSON within 60 seconds including: eventType, timestamp(UTC), actor, channel, invoiceId, clientId, decisionReason, termsSnapshotHash, before/after values, ip, userAgent And audit entries are append-only and filterable by eventType; retention enforced per workspace.security.retentionPolicy And the export contains 100% of events within the requested scope
Dispute-Aware Auto-Pause
"As a therapist, I want reminders to pause automatically if a payment dispute is opened so that I avoid inappropriate follow-ups while the issue is being resolved."
Description

Monitors payment processor webhooks and SoloPilot billing events to detect disputes, chargebacks, refunds, or client-submitted issues. Automatically pauses active nudge sequences for the affected client/invoice, posts an internal alert, and prevents further outreach until resolution. Provides resume/cancel logic based on outcome, maintains an audit trail, and ensures no communications are sent that could aggravate an active dispute.

Acceptance Criteria
Pause on Dispute Webhook Detection
Given an active Straggler Nudges sequence exists for Invoice X And no existing dispute pause is active for Invoice X When the system receives a valid event indicating a dispute, chargeback, refund, or client billing issue for Invoice X from a payment processor webhook or SoloPilot billing event Then the sequence for Invoice X is transitioned to Paused (Dispute) within 60 seconds And all scheduled steps for Invoice X are unscheduled And the pause state is visible on Invoice X and the client's timeline
Idempotent Multi-Event Handling
Given Invoice X is already in Paused (Dispute) When duplicate or subsequent dispute-related events for Invoice X are received (including retries and out-of-order events) Then no additional pause transitions are created And no duplicate internal alerts are posted And the audit trail records the deduplication with the new event IDs linked
Scoped Pause to Affected Client/Invoice
Given Client A has multiple invoices with Straggler Nudges sequences (Invoice X and Invoice Y) When a dispute-related event is received for Invoice X only Then only the sequence linked to Invoice X is paused And the sequence linked to Invoice Y continues unaffected And the internal alert references only Invoice X
Internal Alert and UI Indicator
Given a sequence transitions to Paused (Dispute) for Invoice X When the pause is applied Then an internal alert is created within 60 seconds in Notifications and on the Invoice X and Client A timelines And the alert includes invoice ID, amount, event type, processor reference, timestamp, and deep link to dispute details And the sequence UI displays a Paused by Dispute badge/state until resolution
Communications Block During Active Dispute
Given Invoice X is in Paused (Dispute) When any Straggler Nudges step for Invoice X reaches its scheduled send time across email, SMS, or DM Then the message is not sent And the pending send is marked canceled (Dispute) with a reason code And the delivery queue shows zero attempted sends for Invoice X during the pause window And manual attempts to trigger a Straggler Nudge for Invoice X are blocked with a warning
Resume/Cancel Logic on Outcome
Given Invoice X is in Paused (Dispute) When a resolution event is received indicating dispute won or charge reversed with payment secured Then the sequence resumes at the next unsent step after a 24-hour cooling-off period And a Resume alert is posted with outcome details When a resolution event indicates dispute lost, refund completed, or write-off Then the sequence is permanently canceled with status Canceled (Dispute Resolved - No Collection) And a Cancel alert is posted with outcome details And manual Resume is disabled if any active dispute remains on Invoice X
Audit Trail Completeness and Integrity
Given any pause, resume, or cancel action occurs for Invoice X When viewing the audit log for Invoice X Then entries include action, actor (system or user), timestamp (UTC), source event type and ID, affected channels, number of messages unscheduled/suppressed, and links to related alerts And entries are immutable via the UI and exportable as JSON/CSV And the total counts of canceled/suppressed messages match the number of scheduled messages at time of pause
Consent & Compliance Controls
"As an account owner, I want channel consent and quiet hours enforced automatically so that follow-ups are effective and compliant with regulations."
Description

Manages opt-in/opt-out and legal compliance for messaging channels (email, SMS, DM). Captures and stores consent with timestamp and source, enforces quiet hours and regional rules (e.g., TCPA/GDPR), appends compliant footers and STOP/HELP keywords, and propagates opt-outs across sequences. Provides admin settings per workspace, consent audit logs, per-contact channel preferences, and safeguards to block sends when requirements are not met.

Acceptance Criteria
Capture and Store Channel-Specific Consent
Given a contact provides consent for a specific channel (Email/SMS/DM) via a supported source (web form, API import, in-message keyword) When the consent is saved Then the system records, per channel: consent=true, timestamp in ISO 8601 UTC, source, method, and actor Given the workspace requires double opt-in for Email or SMS When the initial opt-in is captured Then no messages are sent on that channel until confirmation is completed And upon confirmation the consent record is updated with confirmed=true and confirmation timestamp/source Given a consent record exists for a contact/channel When viewed in the consent audit log Then the record is immutable and displays a complete event trail (created, confirmed, updated)
Enforce Quiet Hours and Regional Messaging Rules
Given a contact has a detected region/timezone and the workspace has quiet hours configured for SMS/email/DM When a nudge is scheduled during quiet hours for that channel Then the send is deferred to the next allowable window and an audit entry is created noting the deferment Given a contact is in a region requiring opt-in prior to outreach (e.g., EU GDPR) and no valid opt-in exists for the channel When a send is evaluated Then the send is blocked with reason COMPLIANCE_MISSING_CONSENT and no outbound request is made Given quiet hours and regional rules are applied When the message is eventually sent Then the send timestamp falls within the allowed window for the contact’s local time
Append Compliant Footers and STOP/HELP Keywords
Given channel=Email When a nudge is sent Then the footer includes an unsubscribe link and physical mailing address And clicking the unsubscribe link updates the contact’s Email opt-out immediately and returns a success page (HTTP 200) Given channel=SMS When a nudge is sent Then the message includes the workspace name and "Reply STOP to opt out. Reply HELP for help." And inbound STOP/HELP keywords are recognized and processed accordingly Given channel=DM When a nudge is sent and the platform supports links Then a "Manage preferences" link to the hosted preference center is appended Else text instructions to the preference center URL are appended
Opt-out Handling and Propagation Across Sequences
Given a contact replies STOP to any SMS nudge When the inbound message is processed Then the contact’s SMS channel is set to Opted-Out immediately And a single confirmation SMS is sent And all pending SMS messages across all active sequences are canceled Given a contact clicks an email unsubscribe link When the request is processed Then the contact’s Email channel is set to Opted-Out immediately And no further emails in existing sequences are sent Given a contact updates the hosted preference center to opt out of a channel When the change is saved Then the opt-out is effective immediately across all sequences and an audit event is recorded Given a user attempts a manual send on an opted-out channel When the send is initiated Then the system blocks the send with reason COMPLIANCE_OPTED_OUT
Workspace Admin Compliance Settings and Defaults
Given a workspace admin When they configure per-channel quiet hours by region, consent type (single/double opt-in), and footer templates Then the settings are saved with timestamp, actor, and previous value And the changes take effect for new send evaluations within 5 minutes Given a non-admin user When they open Compliance Settings Then all controls are read-only and changes cannot be saved Given compliance settings were changed When a message is evaluated and sent Then the message audit records the effective rule versions used at evaluation time
Pre-Send Compliance Gate and Dispute Pause
Given a nudge is about to be sent on any channel When the compliance gate runs Then it validates: channel opt-in present, not opted-out, within allowed quiet hours, regional rule satisfied, and no active dispute flag on the contact/invoice And only if all checks pass is the message dispatched Given any validation fails When the send is evaluated Then the message is not sent, status is Blocked, and the specific failure reason code is recorded and visible in logs and UI Given a dispute is opened for a contact’s invoice/payment When the event is received Then all active Straggler Nudges for that contact are paused within 60 seconds And no further messages are sent until the dispute is cleared
Channel Escalation Respects Contact Preferences
Given a Straggler Nudges sequence with escalation Email→SMS→DM When a contact has Email opted-in, SMS opted-out, and DM allowed Then the system sends Email, skips SMS entirely, and schedules DM according to the configured interval Given a contact has all channels opted-out or disallowed by preference When the sequence attempts to send Then no messages are sent And the attempt is recorded with reason NO_ELIGIBLE_CHANNELS And the step is marked Skipped Given a contact updates channel preferences mid-sequence When the next escalation step is evaluated Then the latest preferences are used to determine eligibility
Deliverability & Engagement Tracking
"As a freelancer, I want to see which reminders were delivered and engaged with so that I can adjust my outreach and avoid over-messaging clients."
Description

Integrates with email, SMS, and DM providers to send messages reliably and track delivery, open, click, reply, bounce, and spam events. Provides rate limiting, deduplication, and fallback channel logic when a send fails. Surfaces analytics dashboards and per-client timelines, and feeds engagement signals back into the escalation engine to adjust cadence or cease contact when appropriate. Includes provider health monitoring and message template validation to improve deliverability.

Acceptance Criteria
Normalized Event Tracking Across Channels
Given a message is sent via Email, SMS, or DM with tracking enabled When the provider emits Delivered, Opened, Clicked, Replied, Bounced, or SpamComplaint webhooks Then the system upserts a normalized event record with type, channel, provider, messageId, recipient, timestamp (UTC ms), campaignId, and correlationId And the event appears on the recipient's timeline within 5 seconds of webhook receipt And duplicate or out-of-order webhooks are handled idempotently with causal ordering preserved And Bounce or SpamComplaint events set contact risk flags and suppress future sends per policy
Adaptive Rate Limiting and Queuing
Given outbound sends originate from a Straggler Nudges campaign When planned throughput would exceed the configured per-provider and per-channel limits Then messages are enqueued with jitter to remain at or below limits And provider API 429/5xx responses trigger exponential backoff with a maximum retry window of 15 minutes And the campaign UI shows current send rate, queue depth, and ETA updated at least every 10 seconds
Recipient Deduplication and Compliance Suppression
Given a client has multiple contact methods and past engagement flags When generating a follow-up batch Then no more than one message per channel is sent to the same client within the configured cadence window And contacts with Do-Not-Contact, Unsubscribed, STOP, hard bounce, or spam-complaint flags are excluded with a logged suppression reason And the batch preview displays suppressed counts by reason before send
Automatic Fallback on Send Failure with Dispute Pause
Given an email send returns a hard failure (e.g., hard bounce, invalid address) or the provider is marked degraded When the primary attempt fails Then the system evaluates allowed channels and attempts the next channel (e.g., SMS then DM) within 2 minutes And it does not retry hard failures on the original channel And if the client has an open dispute on the invoice, all escalations are paused and the owner is notified within 1 minute
Engagement-Driven Escalation Adjustments
Given a client opens but does not click the paylink within 24 hours When the escalation engine recalculates cadence Then the next nudge is delayed by 24–48 hours per policy and channel preference remains email And if two emails remain unopened within 72 hours, escalate the next attempt to SMS (if allowed) And any direct reply from the client ceases further automated nudges and assigns a follow-up task to the owner
Dashboards and Per-Client Timelines
Given at least one campaign has run in the selected date range When the user opens Deliverability & Engagement analytics Then the dashboard shows Sent, Delivered, Open Rate, Click Rate, Reply Rate, Bounce Rate, and Spam Complaints by channel, provider, and template with filters for date range, campaign, provider, and channel And metrics reconcile to event counts with variance less than 1% And per-client timelines show ordered events and message copies with sensitive fields redacted, loading within 2 seconds for the last 90 days And CSV export completes within 30 seconds for up to 100,000 rows And the dashboard includes a provider health widget showing current status and error rates
Template Compliance and Deliverability Validation
Given a user saves or activates a message template for email, SMS, or DM When validation runs Then required compliance elements are enforced (e.g., unsubscribe/footer for email, STOP/HELP language for SMS where applicable) And link tracking parameters are present, shortened URLs comply with carrier rules, and message length limits by channel are not exceeded And unresolved personalization tokens or invalid variables block activation with actionable error messages And a deliverability score (0–100) and warnings are presented, and critical failures prevent sending until resolved

Sponsor Billing

Consolidates multiple attendees under a single payer (company or organizer) detected by domain or roster tags. Issues a consolidated invoice with per‑attendee line items, PO fields, and tax IDs, while still sending individual confirmations—making corporate workshops painless and compliant.

Requirements

Sponsor Auto-Association by Domain/Tags
"As an account owner, I want attendee bookings auto-associated to their sponsor by domain or roster tags so that I can invoice the correct payer without manual mapping."
Description

Automatically associate bookings and attendee records to a Sponsor account based on email domain matching and/or predefined roster tags. Supports priority rules (explicit roster tag overrides domain, manual override supersedes both), retroactive association of historical sessions, and real-time assignment at booking time. Includes deduplication of sponsors by domain, conflict detection when multiple sponsors match, and an audit trail of association changes. Integrates with scheduling and invoicing so that sponsor associations drive eligibility for consolidated billing and pricing logic without manual mapping.

Acceptance Criteria
Real-Time Domain Association at Booking
Given a Sponsor with verified domain "acme.com" exists and the attendee has no roster tag mapped to a Sponsor And an attendee books a session using "jane@acme.com" When the booking is submitted Then the attendee and booking are automatically associated to the "Acme" Sponsor And the association source is recorded as "domain" And the association is visible on the booking and attendee records immediately And if no Sponsor matches the email domain, no Sponsor association is set
Roster Tag Overrides Domain
Given a roster tag "ACME-COHORT-A" is mapped to Sponsor "Acme" And an attendee has tag "ACME-COHORT-A" and uses an email whose domain matches a different Sponsor When the attendee books a session Then the attendee and booking are associated to Sponsor "Acme" And the association source is recorded as "roster-tag" And the domain match is ignored for this association
Manual Override Supersedes Auto Rules
Given an attendee record currently auto-associated to Sponsor "Acme" by domain or tag When a user manually changes the Sponsor to "Globex" and saves Then the attendee and all future bookings default to "Globex" for association And auto-association rules do not overwrite the manual selection And a "manual-override" flag is stored on the record And when the manual override is cleared by a user, Then auto-association is re-enabled on the next booking or update
Retroactive Association Backfill
Given Sponsor rules (verified domains and roster tags) are configured And historical attendees and sessions exist without manual overrides When a backfill job is run Then records matching the rules are associated to the correct Sponsor And records that already have a conflicting manual Sponsor are skipped And a summary report lists counts of associated, skipped, and conflicted records
Domain Deduplication and Conflict Prevention
Given a Sponsor exists with verified domain "acme.com" When a user attempts to add "acme.com" as a verified domain to another Sponsor Then the system blocks the action and indicates the domain is already in use And any new domain-based associations for "acme.com" resolve to the unique owning Sponsor And if duplicates already exist from legacy data, a merge flow is presented and only one Sponsor retains the domain after completion
Consolidated Billing and Pricing Integration
Given a booking is associated to Sponsor "Acme" When a consolidated invoice is generated for "Acme" Then the booking appears as a per-attendee line item on the Sponsor's consolidated invoice And the Sponsor's PO number and tax ID appear on the invoice And the attendee receives an individual session confirmation email And the billed rate follows the Sponsor's pricing rules And no individual invoice is created for the attendee for that session And when the Sponsor association is changed prior to invoice issuance, the line item eligibility and pricing rules update accordingly on the next run
Audit Trail of Sponsor Association Changes
Given any Sponsor association is created, updated, or removed When the change occurs via domain match, roster tag, manual override, or backfill Then an audit entry is written capturing timestamp, actor (user or system), source ("domain", "roster-tag", "manual", "backfill"), previous Sponsor, new Sponsor, and affected record IDs And the audit log is read-only and filterable by source, Sponsor, and date range
Consolidated Invoice with Per-Attendee Line Items
"As a consultant, I want to issue one invoice to a company with per-attendee line items so that they can pay once while keeping detailed audit trails."
Description

Generate a single invoice per Sponsor per billing cycle that aggregates all eligible sessions into per-attendee line items. Each line item includes attendee name, session date/time, service type/code, rate, quantity/duration, taxes, and notes. Invoice supports custom fields (PO number, cost center, project code), sponsor tax IDs, currency, payment terms, and due dates. Provides configurable grouping rules (by date range, program, location), preview and regenerate options with idempotent numbering, and PDF/export/email delivery to sponsor billing contacts. Links each line item to its source session and attendee record for auditability.

Acceptance Criteria
Single Consolidated Sponsor Invoice per Billing Cycle
Given a sponsor with a defined billing cycle and multiple eligible sessions across attendees When generating invoices for that cycle Then exactly one open invoice is created for the sponsor for that cycle And the invoice contains one line item per eligible session-attendee pair And sessions outside the cycle window, already invoiced, or flagged non-billable are excluded And the invoice total equals the sum of included line item totals
Per-Attendee Line Item Completeness and Accuracy
Given a generated sponsor invoice with mixed services and durations When viewing the invoice line items Then each line item shows attendee full name, session local date and start time with timezone, service name and billing code, unit rate, quantity or duration, tax amount and tax rate, and notes if present And line item subtotal equals unit rate multiplied by quantity or duration as configured And tax per line item is computed per jurisdiction settings and rounding rules And invoice subtotal equals the sum of line item subtotals And invoice tax equals the sum of line item taxes And invoice grand total equals subtotal plus tax in the invoice currency
Sponsor Detection and Session Eligibility
Given attendees matched to a sponsor by email domain or roster tag and others without a match When generating the sponsor invoice for a cycle Then only sessions belonging to matched attendees within the cycle window are eligible And sessions with a manual sponsor override to a different payer are excluded And sessions paid personally are excluded And each included or excluded session is logged with eligibility reason
Custom Fields and Sponsor Tax Identifiers on Invoice
Given sponsor profile contains PO number, cost center, project code, and tax ID fields and some are marked required When generating the invoice Then all configured custom fields appear on the invoice header and PDF/export outputs And required fields must be present or invoice generation is blocked with a clear validation error And invoice currency is set from sponsor defaults unless an allowed override is specified at program level And payment terms and due date are applied per sponsor terms so that due date equals issue date plus net terms days
Configurable Grouping Rules and Cycle Cutoffs
Given grouping is configured by date range monthly and by program and optional location When generating invoices for the period 2025-09-01 00:00 to 2025-09-30 23:59 in the workspace timezone Then only sessions starting within that window are considered And line items are grouped under program sections and annotated with location when enabled And proration or minimum charge rules are applied exactly as configured And editing grouping settings and regenerating updates the invoice contents without duplicating or dropping eligible sessions
Preview, Regenerate, and Idempotent Numbering
Given an invoice preview exists for a sponsor and cycle When regenerating with unchanged inputs Then the invoice number remains unchanged and content reflects current eligible sessions And regenerating with materially changed inputs such as sponsor or cycle creates a new invoice with a new number and voids the prior preview with an audit entry And invoice numbers are unique and sequential within the sponsor billing series And all regenerate actions are captured in an immutable audit log with actor, timestamp, and change summary
Delivery, Export, and Auditability
Given an approved invoice ready for delivery When sending to sponsor billing contacts Then emails are sent to all active billing contacts with a branded PDF and portal link and delivery status is tracked And attendees receive individual session confirmations without pricing And exports are available as PDF for the invoice and CSV for line items including session ID, attendee ID, service code, rate, quantity, tax, and notes And each line item links to its source session and attendee record in the UI for audit And all delivery events, opens, bounces, and downloads are logged and visible to authorized users
Sponsor Compliance Fields & Validation
"As a finance admin, I want PO and tax fields validated before invoicing so that invoices are accepted without back-and-forth."
Description

Maintain a Sponsor profile with required billing and compliance metadata, including legal name, billing address, VAT/GST/Tax IDs, PO requirements, and custom field templates. Enforce field completeness and country-specific format validation prior to invoice issuance. Support multiple billing contacts and remittance instructions, secure storage of sensitive fields, and an audit log of changes. Provide API/webhook mapping for PO and tax fields to downstream accounting systems. Prevent invoice dispatch until mandatory compliance fields are satisfied, with clear validation errors for users.

Acceptance Criteria
Field Completeness & Country-Specific Validation Prior to Invoice
Given a Sponsor profile has a country set and one or more mandatory compliance fields are missing or invalid When a user attempts to issue or schedule an invoice to that Sponsor Then invoice issuance is blocked And a validation panel lists each missing field and each format error with field-level messages And the required field set is derived from country rules and Sponsor-specific PO requirements And saving the Sponsor profile is allowed regardless, but dispatch requires all validations to pass And when all required fields are present and pass country-specific format checks, invoice issuance proceeds
Custom Compliance Field Templates Enforcement
Given an admin defines compliance custom fields (name, type, required flag, validators, export/display flags) at workspace or Sponsor scope When editing a Sponsor profile Then the custom fields render with correct input types and helper text And custom fields marked required must be completed before invoice issuance And validators (regex/range/enumeration) are enforced with specific error messages And custom fields flagged “include on invoice” appear on the invoice PDF/email in the designated section And custom fields flagged “export via API” are included in outbound payloads using their stable keys
Multiple Billing Contacts & Roles
Given a Sponsor profile supports multiple billing contacts with roles (Primary, Accounts Payable, Approver) When saving contacts Then exactly one Primary contact is required And each contact may have notification preferences (invoice, reminders, confirmations) And when an invoice is issued, notifications are sent to contacts per their preferences And if no contact has a valid email for any selected notification type, issuance is blocked with a clear error And contact changes do not alter historical invoices’ recipient lists retroactively
Remittance Instructions on Invoices
Given a Sponsor profile has one or more remittance instructions (e.g., bank transfer, ACH) with currency and region applicability When an invoice is generated for that Sponsor Then the default remittance instruction for the invoice currency is selected automatically And the selected instruction is rendered on the invoice PDF and email exactly as stored And users with permission may override the selection at invoice time; others cannot And inactive or expired remittance instructions cannot be selected And changes to remittance instructions do not modify previously issued invoices
Invoice Dispatch Gating & Validation Errors UX
Given a user clicks Send Invoice for a Sponsor with missing or invalid compliance data When validation runs Then the Send action is prevented and disabled until all errors are resolved And a banner summarizes the number of issues and links to each offending field And each field displays a specific, actionable error (including format example where applicable) And clicking Fix opens the Sponsor pane focused on the first failing field And upon correcting all issues, the banner clears and Send re-enables without page reload
Sensitive Fields Security & Audit Log
Given sensitive compliance fields (tax IDs, PO numbers, banking data) exist Then values are encrypted at rest and masked in UI by default And only users with Billing Admin permission can view unmasked values And every create/update/delete of compliance fields writes an immutable audit record including actor, UTC timestamp, previous value hash, new value hash, and origin (UI/API) And viewing unmasked sensitive values is logged as an access event And audit logs are filterable by Sponsor, field, actor, and date range and exportable by Billing Admins
API/Webhook Mapping for PO, Tax, and Custom Fields
Given accounting integrations are enabled When an invoice for a Sponsor is issued or updated Then the outbound webhook payload includes sponsor_legal_name, billing_address, country, tax_id.value, tax_id.scheme (e.g., EU_VAT, US_EIN), po_number, and all custom fields flagged for export with stable keys and types And fields not applicable are omitted, not sent as null/empty strings And the payload conforms to the published schema version and passes JSON schema validation And a test webhook in Settings can be triggered to preview the exact payload for a selected Sponsor And any payload mapping change increments the schema version and is recorded in the audit log
Attendee Confirmations Without Pricing (Sponsor-Paid)
"As an attendee, I want to receive my own confirmations without pricing so that I have the details while my company handles payment."
Description

Send individual session confirmations, reminders, and updates to attendees covered by a Sponsor while suppressing pricing and payment prompts. Templates include program details, logistics, cancellation policy, and a sponsor note. Fallback logic prompts self-pay only if no sponsor applies at the time of booking or if eligibility changes. Supports series bookings, reschedules, and cancellations, ensuring communications remain consistent with sponsor coverage. Integrates with existing SoloPilot automation rules and respects notification preferences and locale.

Acceptance Criteria
Sponsor-Paid Confirmation Email Omits Pricing and Payment Prompts
Given an attendee is associated to an active Sponsor for the booked session(s) When the system generates the confirmation after booking Then the confirmation contains program title, date/time, location/meeting link, prep/logistics, and cancellation policy And the confirmation contains the sponsor note if configured for the Sponsor And the confirmation does not contain any prices, currency symbols, invoice totals, or payment CTAs/links/buttons And the confirmation template selected is the sponsor-paid variant for the attendee’s locale And all merge fields resolve without placeholder artifacts
Self-Pay Fallback When No Sponsor Detected at Booking
Given an attendee has no Sponsor coverage at booking time When the confirmation is generated Then the self-pay confirmation is sent including pricing and a payment CTA per product settings And no sponsor note is included And invoice link or payment instructions are present according to automation rules
Coverage Change Updates Future Communications to Match Current Eligibility
Given a booking initially marked sponsor-covered And Sponsor coverage is removed or expires before the session When the system evaluates scheduled communications Then all future reminders and updates switch to the self-pay variant with pricing and payment CTAs And previously sent messages are not altered or resent And any scheduled payment reminders previously suppressed due to sponsor are re-enabled for future messages
Series Booking Communications Stay Sponsor-Compliant
Given an attendee books a series where sessions are sponsor-covered When the system sends the initial series confirmation Then the series summary and each session detail exclude all pricing and payment prompts And per-session reminders inherit the sponsor-paid variant And if coverage is mixed across sessions, each session’s communications reflect that session’s coverage status at send time
Reschedules and Cancellations Maintain Sponsor Coverage Rules
Given a sponsor-covered session is rescheduled by organizer or attendee When the reschedule confirmation and updated reminders are sent Then the communications exclude pricing and payment prompts and include sponsor note and logistics for the new time Given a sponsor-covered session is canceled When the cancellation notice is sent Then the notice excludes pricing/payment prompts and includes the cancellation policy text And if the attendee is self-pay, the reschedule/cancellation communications include pricing, payment links, and any configured fees per product settings
Notification Preferences and Locale Are Honored
Given an attendee has notification preferences (email/SMS enabled/disabled) and a locale When confirmations, reminders, or updates are sent Then messages are delivered only via enabled channels And content is localized to the attendee’s locale including date/time formats, currency symbols if applicable, and translated templates And if locale is unsupported, the default locale template is used without placeholders And do-not-disturb windows and unsubscribes for marketing are respected while still delivering transactional notifications
Automation Rules Integration and No Duplicate Messaging
Given SoloPilot automation rules are active for confirmations and reminders When a booking is sponsor-covered Then the automation triggers the sponsor-paid template variants and suppresses any payment-related steps And exactly one confirmation per booking event is sent per configured channel (no duplicates) And updating coverage does not create duplicate sends; only scheduled future messages swap template variants
Payment Allocation & Reconciliation to Sessions
"As a business owner, I want sponsor payments to automatically mark underlying sessions as paid so that my books and schedules stay accurate."
Description

Record payments collected against consolidated sponsor invoices and automatically allocate amounts to underlying line items and sessions. Update session financial state to Paid by Sponsor, handle partial payments, short-pays, write-offs, and credit memos, and support refunds that re-open affected items. Provide reconciliation views showing open balances by sponsor and by attendee, plus exports to accounting. Include dunning triggers for overdue invoices and guardrails to prevent double-allocation or orphaned payments.

Acceptance Criteria
Full Payment Auto-Allocation to Sessions
Given a consolidated sponsor invoice with N line items linked to sessions and status "Open" When a payment equal to the invoice total is recorded against the invoice Then the system allocates 100% of the payment to all line items in invoice line order And each line item shows Paid Amount = Line Amount and Remaining Balance = 0.00 And each associated session financial state updates to "Paid by Sponsor" And the invoice status becomes "Paid" And the payment shows Unapplied Amount = 0.00 And an allocation audit record is created with payment ID, allocation map (line item ID → amount), timestamp, and actor
Partial Payment Allocation and Session State Updates
Given a consolidated sponsor invoice with multiple ordered line items and open balances When a partial payment amount P less than the invoice open balance is recorded Then the system allocates funds to line items in invoice line order until P is exhausted And lines fully covered show Remaining Balance = 0.00; the next line shows a reduced Remaining Balance; subsequent lines remain unchanged And only sessions whose line items are fully funded update to "Paid by Sponsor"; partially funded sessions do not change state And the invoice status becomes "Partially Paid" and Open Balance decreases by P And the payment shows Unapplied Amount = 0.00 And an allocation audit record is created capturing the allocation amounts per line item
Refund Processing Reopens Affected Items
Given an invoice with prior payment allocations across line items and sessions marked "Paid by Sponsor" When a refund R not exceeding the total allocated amount is issued against a specific payment Then the system posts a negative payment of amount R linked to the original payment And de-allocates funds in reverse allocation order (most recent allocations first) until R is fully reversed And any line item whose Paid Amount falls below its Line Amount reopens with an increased Remaining Balance And associated sessions for reopened line items revert from "Paid by Sponsor" to "Unpaid" And the invoice status recalculates to "Paid", "Partially Paid", or "Open" as appropriate And an audit record of the refund and de-allocation is created
Adjustment Handling for Short-Pays and Credit Memos
Write-Offs: Given an invoice with a small remaining balance B > 0 When an authorized user records a write-off of amount W where 0 < W <= B and selects a reason code Then the system creates a write-off adjustment, reduces the invoice open balance by W, and updates affected line items' Remaining Balances accordingly And any line item now fully resolved is marked Paid; associated sessions update to "Paid by Sponsor" And if the open balance reaches 0.00, the invoice status becomes "Paid" And the write-off is included in reporting and exports with reason code and timestamp Credit Memos: Given a sponsor has an unapplied credit memo of amount C and an open invoice with balance B > 0 When a user applies a credit amount A where 0 < A <= min(C, B) to that invoice Then the invoice open balance decreases by A and the credit memo balance decreases by A And the system allocates A to line items in invoice line order, updating Paid Amounts and session states for fully funded items And links between the credit memo and applied invoice/line items are stored for traceability And an audit record is created for the credit application
Reconciliation Views and Accounting Export
Sponsor Reconciliation View: Given sponsors with multiple invoices, payments, credits, refunds, and write-offs exist When a user opens the Sponsor Reconciliation view Then each sponsor row displays Total Billed, Total Paid, Credits/Refunds/Write-offs (net), and Open Balance And clicking a sponsor shows invoice-level detail and an attendee breakdown with each attendee's Total Billed, Total Paid, and Open Balance And all subtotals and totals reconcile within 0.01 currency units to the sum of underlying transactions Export to Accounting: Given the reconciliation view data is available When a user exports to CSV and JSON Then each exported line includes Sponsor ID/Name, Attendee ID/Name, Invoice ID, PO Number, Tax ID, Session ID/Date, Currency, Line Amount, Tax Amount, Paid Amount, Remaining Balance, Payment IDs, Credit/Refund/Write-off indicators, and Timestamps And the export passes schema validation and column totals match the UI totals for the same filters and date range
Guardrails Against Double-Allocation and Orphaned Payments
Given a payment or credit is being applied to an invoice line item When a concurrent apply operation attempts to allocate to the same payment or line item Then the system enforces atomic allocation and prevents double-allocation so that Paid Amount never exceeds Line Amount And any over-application attempt returns a validation error and no partial allocations are committed And When attempting to allocate to a closed/deleted invoice or a non-existent line item Then the system rejects the operation with a validation error and leaves the payment unapplied And When voiding/deleting an invoice that has applied payments/credits Then the system requires de-allocation first or auto-reverses allocations safely so no orphaned applied amounts remain and all session states reflect the resulting balances
Overdue Dunning Triggers for Sponsor Invoices
Given a sponsor invoice has Open Balance > 0 and Due Date in the past by at least D days where D >= the configured dunning threshold When the scheduled dunning job runs Then the invoice is assigned Dunning Level 1 (or next level based on prior history) and a dunning notice is sent to the sponsor billing contact including invoice number, PO, amount due, due date, and payment link And a next dunning date is set per the configured cadence and dunning stops automatically when Open Balance = 0 And When an invoice is marked "On Hold" or "In Dispute" Then dunning notices are suppressed until the hold/dispute is cleared And All dunning events and outcomes are logged on the invoice timeline with timestamps
Roster Import & Tagging for Sponsored Attendees
"As a workshop organizer, I want to upload a roster and tag attendees to a sponsor so that consolidated billing reflects the correct participants."
Description

Enable upload of attendee rosters via CSV or copy-paste, with column mapping for email, name, tags, and program. Bulk-apply sponsor roster tags, set eligibility dates, and resolve duplicates by email. Changes to rosters update sponsor associations for future bookings and optionally for unsent invoices. Provide validation feedback, preview of impacted attendees, and activity logs. Supports scheduled roster refresh via secure link or integration hook to keep eligibility current ahead of workshops.

Acceptance Criteria
CSV Upload with Column Mapping
Given I am on the sponsor’s roster import screen And I have a CSV with headers for Email, Name (or First Name/Last Name), Tags, and Program (optional Eligibility Start/End) When I upload the CSV Then the system presents a column-mapping UI with auto-detected suggestions for common headers And I can manually map, ignore, or set default values for unmapped fields And the system validates that each row has a well-formed, non-empty Email; invalid or missing emails are flagged with row numbers When I confirm the mapping Then a preview displays counts of New, Updated, and Skipped attendees And proceeding completes the import, applying mappings and associating attendees to the selected sponsor And a success summary shows totals for imported, updated, and skipped rows
Copy-Paste Roster Parsing
Given I open the copy-paste import option on the sponsor’s roster screen And I paste tabular data from a spreadsheet When the system auto-detects the delimiter (or I choose comma/tab/semicolon) Then the mapping UI renders the first 50 rows for verification and column mapping And Email fields are validated in real-time with inline error flags When I confirm mapping and proceed Then a preview shows New/Updated/Skipped counts and sample rows And completing the import applies identical validation and association rules as CSV upload
Bulk Tagging and Eligibility Windows
Given I select one or more sponsor roster tags to apply to all imported attendees And I optionally set Eligibility Start and/or End dates for the sponsor When I run the import Then each attendee is assigned the selected sponsor roster tags And an eligibility window is stored per attendee for this sponsor And sessions booked within the eligibility window auto-associate to the sponsor for billing And sessions outside the window do not auto-associate And if dates are omitted, eligibility starts on the import date and remains open-ended in the sponsor’s timezone
Duplicate Resolution by Email
Given the roster contains emails that already exist in the workspace (case-insensitive, trimmed) When I import the roster Then matching attendees are updated rather than duplicated And new tags from the import are merged without removing unrelated existing tags And Program and eligibility window values are updated per the import And existing notes and history remain unchanged And the preview and summary report counts of Updated vs New vs Skipped and list duplicate-resolution examples
Update Sponsor Associations and Unsent Invoices
Given there is an option to apply roster changes to unsent invoices And there exist unsent invoices for sessions that become eligible due to this import When I enable the option and complete the import Then future bookings for eligible attendees auto-associate to the sponsor And unsent invoices are updated to set the sponsor as payer, populate PO/tax IDs from the sponsor profile, and recalculate per-attendee line items And sent or paid invoices are not modified And the preview shows how many invoices will be updated before confirmation And an activity log records each invoice update with before/after payer details
Validation Feedback, Preview, and Activity Logs
Given the import contains validation issues (e.g., invalid email, end date before start date, missing required mappings) When I attempt to proceed Then the system blocks the import and displays inline errors with row numbers and reasons And I can download a CSV error report When all blocking errors are resolved and I complete the import Then a preview lists impacted attendees with status (New/Updated/Skipped) and skip reasons And an immutable activity log entry is created with actor, timestamp, counts, and a link to the preview And admins can view the log under Sponsor Billing activity
Scheduled Roster Refresh via Secure Link or Integration Hook
Given I configure a scheduled roster refresh using a secure pull URL with token authentication or an integration hook And I set a refresh frequency (e.g., daily at 02:00 sponsor timezone) When the schedule runs Then the system fetches the roster, applies the saved mapping, and upserts attendees by email And eligibility windows and tags are updated per the latest roster And the process is idempotent; unchanged rows do not create new changes And failures trigger admin notifications with error details; successes log import counts And each run is recorded in the activity log with Pass/Fail status and a correlation ID And tokens can be rotated; old tokens expire immediately after rotation

WORM Sentinel

Locks every note, upload, and edit as append‑only with cryptographic fingerprints and chain‑of‑custody receipts. You get instant proof of integrity and a clear addendum trail for corrections—no risky overwrites. Tamper signals trigger alerts and a verification report you can hand to auditors or insurers in seconds.

Requirements

Append-Only Storage Enforcement
"As an independent practitioner, I want my records to be append-only so that no one can alter or delete past entries and my documentation remains defensible."
Description

Enforce an immutable, append-only write model across notes, file uploads, and edit events within SoloPilot. All create and update operations must produce new versions that are linked to prior versions; destructive actions (overwrite, delete) are blocked at API, database, and storage layers. Configure storage with retention locks (e.g., object-lock compliance for files) and append-only event tables for metadata. Provide UI cues showing immutability, retention timers, and legal hold states. Support privacy needs via non-destructive redaction addenda rather than deletion. Applies to session notes, client attachments, and automated system edits to ensure defensible recordkeeping and prevent accidental or malicious tampering.

Acceptance Criteria
API-Level Append-Only Enforcement
- Given an existing note/file/edit event, when a client issues PUT or PATCH intended to overwrite the latest version, then the API responds 409 Conflict with error_code=APPEND_ONLY_ENFORCED and no data is mutated. - Given a DELETE request to any note/file/version endpoint, then the API responds 405 Method Not Allowed (or 403 per policy) and persists an audit_log entry with outcome=blocked_delete and target identifier. - Given a valid update intent, when POST /{resource}/{id}/versions is called, then a new version is created with version_id != prior.version_id, prior_version_id = prior.version_id, created_at > prior.created_at, and the previous version remains readable. - Given concurrent POSTs for the same resource, when processed, then the system serializes creation so the later version’s prior_version_id equals the immediate predecessor and no overwrites occur.
Database Append-Only Constraints and Version Linkage
- Given direct DB access, when executing UPDATE on versions tables (notes_versions, files_versions, edit_events), then the database rejects the statement and zero rows are modified. - Given direct DB access, when executing DELETE on versions tables, then the database rejects the statement and zero rows are removed. - Given a newly created version, then the row contains non-null fields: version_id (UUID), resource_id, version_number (monotonic), prior_version_id (nullable only for first), content_hash (SHA-256), prior_hash (matches hash of prior_version_id), actor_id, action_type, created_at (immutable), and unique(resource_id, version_number) holds. - Given a resource with N versions, when selecting ordered by version_number, then the chain is contiguous (no gaps) and for all n>1, SHA256(bytes(n-1)) == versions[n].prior_hash.
Object Storage WORM Retention and Legal Holds
- Given a file upload, when stored, then the object is created with Object Lock in Compliance mode and retention_until >= now + configured_min_retention; the API returns retention_until. - Given an attempt to overwrite the object key before retention_until, then PutObject/CopyObject is denied by storage (AccessDenied) and no new content replaces the existing object. - Given an attempt to delete the object before retention_until, then DeleteObject is denied and an audit_log event (blocked_delete, storage_error_code) is recorded. - Given an authorized admin enables legal hold, then GetObjectLegalHold returns ON and UI/API reflect legal_hold=true; unauthorized attempts to set/clear legal hold are denied and audit-logged.
UI Immutability and Retention Indicators
- Given a user opens a note/file, then the UI displays: Immutable badge, version_number, created_at, short content_hash, short prior_hash, and a retention countdown to retention_until. - Given the resource is under legal hold, then the UI shows a Legal Hold indicator and disables inline edit/delete controls; only Add Addendum/Upload New Version actions are enabled. - Given the user attempts an inline edit or delete, then the UI prevents the action, shows an explanatory message, and routes to Create New Version (for edit) with no destructive API call issued.
Non-Destructive Redaction Addenda
- Given a user with redact permission submits a redaction (scope + reason) on a note, then the system creates a new version with action_type=redaction_addendum, original content remains in the prior version, and masked segments appear in default views. - Given role=standard_user views the redacted note, then masked segments are obscured and search indexes reflect redacted text; given role=auditor performs an explicit Reveal Original action, then the original content is displayed and the reveal is audit-logged with reason and actor. - Given a data export, when mode=client_export, then only redacted content is exported; when mode=auditor_export, then both original and redaction addendum are exported with linkage and fingerprints.
Automated System Edit Events Are Append-Only
- Given a background automation updates a session note, then the system writes a new version with source=system, automation_id, and prior_version_id set; no previous version data is altered. - Given a background process attempts a destructive DB/API operation on versions, then the operation is denied at the respective layer and an alert plus audit_log entry with process identity is recorded. - Given multiple system updates in quick succession, then versions are appended with strictly increasing created_at and version_number, and the UI timeline labels them as System with the automation name.
Cryptographic Fingerprints and Verification Report
- Given any new version is created, then content_hash = SHA-256(canonicalized_content_bytes) and prior_hash = content_hash of the immediate predecessor; both are stored immutably and returned by the API. - Given a user requests Verify Chain for a resource, then the system recomputes hashes end-to-end and returns a signed report with status (valid/invalid), first failing link if any, resource_id, latest_version_id, latest_hash, timestamp, and signer key ID. - Given a tamper condition (e.g., DB row modified out of band) causing hash mismatch, then verification status becomes invalid, the resource is flagged in the UI, and an alert is emitted to configured channels within 60 seconds.
Cryptographic Fingerprinting & Chain Linking
"As a compliance-sensitive user, I want cryptographic fingerprints linked across versions so that I can prove a record’s integrity at any time."
Description

Generate a cryptographic fingerprint for every note, file, and addendum using a modern hash (e.g., SHA-256) and link each version to its predecessor to form an integrity chain per record. Persist hash, previous hash, canonical record ID, timestamp, author, device/session identifiers, and content byte-size in an append-only ledger. Periodically commit chain checkpoints (Merkle roots) to strengthen tamper evidence. Expose verification endpoints to recompute and compare hashes on demand. This ensures end-to-end integrity proofs across the lifecycle of each artifact.

Acceptance Criteria
SHA-256 Fingerprint on Artifact Creation
Given a new artifact (note, file upload, or addendum) is saved When the system persists the artifact Then it computes the SHA-256 hash over the exact stored byte sequence and stores it as artifactHash And artifactHash equals the recomputed SHA-256 of the stored bytes on subsequent verification And the configured hash algorithm defaults to SHA-256 and cannot be configured to any algorithm weaker than SHA-256 And the hash value is encoded as lowercase hex without separators and is exactly 64 characters long
Integrity Chain Linking per Canonical Record
Given an artifact with canonicalRecordId has one or more versions When a new version is appended Then the ledger entry stores previousHash that exactly matches the artifactHash of the immediate prior version of the same canonicalRecordId And the first version (genesis) stores previousHash as null (or a defined zero-value constant) and is accepted only if no prior version exists And versions for a canonicalRecordId are strictly time-ordered by persisted timestamp and cannot be backdated relative to existing entries And recomputing links from genesis to head yields an unbroken chain with no missing or duplicate sequence positions
Append-Only Ledger Enforcement
Given an existing ledger entry When a client attempts to update, overwrite, or delete the entry by ID Then the operation is rejected and no existing ledger rows are modified And only appending a new entry is allowed to reflect changes (as an addendum) And the API returns HTTP 409 Conflict for overwrite attempts and HTTP 405 Method Not Allowed for delete attempts And the audit log records the denied attempt with actor, timestamp, and reason code IMMUTABLE_LEDGER
Ledger Field Completeness and Accuracy
Given a ledger entry is written for any artifact event When the entry is inspected Then it contains non-null values for: artifactHash, previousHash (nullable only for genesis), canonicalRecordId, timestamp (ISO 8601 UTC), authorId, deviceId or sessionId, and contentByteSize (integer) And contentByteSize equals the number of bytes used to compute artifactHash And canonicalRecordId is stable across all versions of the same artifact and differs across unrelated artifacts And timestamp is within 2 seconds of server time at write and is immutable thereafter And authorId and session/device identifiers correspond to an active authenticated session at time of write
Periodic Merkle Checkpoint Emission and Proof
Given checkpoint.interval is configured to 15 minutes When the system reaches each interval boundary Then it emits a Merkle root covering all ledger entries committed in that interval and persists: rootHash (hex), intervalStart, intervalEnd, treeHeight, totalLeaves And emission occurs within 60 seconds after the boundary And for any ledger entry within the interval, the system can return a Merkle inclusion proof that validates against the stored rootHash And recomputing the root from leaves and the inclusion proof matches the stored rootHash exactly
On-Demand Verification Endpoints for Hash, Chain, and Checkpoint
Given a canonicalRecordId with N versions exists When a client calls GET /worm/verify/chain/{canonicalRecordId} Then the response is 200 with JSON containing: recordId, chainValid (bool), headHash, headTimestamp, breaks (array of indices if any), checkedAt (ISO 8601 UTC) And chainValid is true only if every previousHash links correctly and recomputed hashes match stored hashes across all N versions And for any single version ID, GET /worm/verify/version/{id} returns 200 with storedHash, computedHash, match (bool) And for any checkpoint ID, GET /worm/verify/checkpoint/{checkpointId} returns 200 with rootHash, proofValid (bool) for a provided leaf, and details And verifying a chain of up to 1,000 versions completes within 2 seconds p95 under nominal load
Addendum Workflow & Correction Trails
"As a practitioner, I want to add corrections via addenda instead of editing originals so that mistakes are transparently traceable without risking data loss."
Description

Provide a structured addendum workflow for corrections and clarifications without altering originals. Users can create addenda that reference a specific prior version, supply reason and category, and attach supporting files. The system displays a consolidated timeline with clear visual diffs, timestamps, and author attributions. Permissions restrict who can author addenda and who can view sensitive addendum details. Each addendum is cryptographically linked to the referenced version, preserving a transparent correction trail suitable for audits and clinical/legal review.

Acceptance Criteria
Author Adds Addendum to Specific Version
Given I have Addendum:Author permission and access to a note's version history And a prior version {versionId} exists When I choose "Add Addendum" for {versionId} and provide a reason (meets configured min length) and select a category from the configured taxonomy And I optionally attach files that meet configured type/count/size limits And I submit Then the system creates an immutable addendum linked to {versionId} And the original content remains unchanged and read-only And the addendum is assigned a unique ID and ISO 8601 UTC timestamp And I see a success message showing the Addendum ID and link
Consolidated Timeline With Diffs and Attributions
Given a note has ≥1 addendum When I open the Correction Timeline view Then timeline entries are sorted by timestamp descending And each entry displays author name and ID, role, timestamp (ISO 8601 UTC), reason, and category And a visual diff panel shows addendum-specific additions relative to the referenced version, using the standard color legend And selecting an entry reveals full addendum text and its attachments And all timestamps and user attributions match the underlying records
Authoring Permissions Enforcement
Given I lack Addendum:Author permission When I view a note's version history Then the "Add Addendum" action is not visible or is disabled with rationale per design And any direct API attempt to create an addendum returns HTTP 403 with error code ADDENDUM_AUTHZ_DENIED and is audit-logged with my user ID and IP Given I am granted Addendum:Author permission When I refresh the same page Then the "Add Addendum" action becomes available for records within my access scope
Sensitive Addendum Visibility Controls
Given an addendum is marked Sensitive with visibility groups configured When an unauthorized user views the Correction Timeline Then the sensitive addendum entry displays a Restricted placeholder for body, reason, and attachments And no attachment download links are rendered And the attempt is audit-logged with user ID, timestamp, and outcome DENIED When an authorized user in the visibility group views the same entry Then full body, reason, and attachments are visible and downloadable
Cryptographic Linkage and Chain Verification
Given an addendum is created Then the system computes a content fingerprint and stores a chain link that includes the referenced version fingerprint, addendum fingerprint, author ID, and timestamp And the append-only store rejects any update or delete to addenda or links and audit-logs the attempt When the chain verification endpoint is called for the note Then it returns Valid with the recomputed chain state for all links, or Invalid with the first failing link identified
Audit-Ready Correction Trail Export
Given a note with addenda exists and I have Addendum:Export permission When I request Export Correction Trail Then the system generates a human-readable PDF and a machine-readable JSON that include the full timeline, author attributions, timestamps, reasons, categories, addendum IDs, referenced version IDs, and fingerprints And the export files are signed and include verification metadata (signature, algorithm, key identifier) And the export is available for download within the configured SLA and is audit-logged
Chain-of-Custody Receipts & Verification Report
"As a user facing audits, I want chain-of-custody receipts and a one-click verification report so that I can quickly demonstrate data integrity without exposing sensitive content."
Description

Emit a signed chain-of-custody receipt on every commit containing content hash, previous hash, timestamp, actor identity, IP/location metadata, and key version. Store receipts with the record and make them downloadable per record or as a time-bounded bundle. Provide a one-click verification report that summarizes integrity checks for a client, matter, or date range, including any gaps or anomalies, and exportable as PDF and machine-readable JSON. The report must never expose protected content, only metadata fingerprints and verification outcomes, enabling sharing with auditors, insurers, or courts.

Acceptance Criteria
Receipt Emission on Commit
Given an authenticated user commits a new note, upload, or edit to a record When the commit is persisted Then a receipt is created and cryptographically signed at commit time And the receipt includes fields: record_id, commit_id, content_hash, previous_hash, timestamp_utc, actor_identity, ip_address, location.country_code, key_version, signature And the signature validates against the public key for key_version And previous_hash equals the content_hash of the immediately preceding commit for the same record, or is null for the first commit And the receipt is stored immutably with the record and cannot be modified or deleted via API/UI (attempts return 403) And the receipt is retrievable via UI and API by record_id and commit_id
Per-Record Receipt Download
Given a user with permission views a record with one or more receipts When they select Download Receipts for that record Then the system returns a downloadable JSON file containing all receipts for the record in chronological order by timestamp_utc and commit_id And the file includes a manifest with record_id, total_receipts, time_range, file_hash, generation_timestamp And the download completes within 5 seconds for up to 1,000 receipts And the request and download are audit-logged with user_id, record_id, and timestamp
Time-Bounded Receipt Bundle Export
Given a user specifies a date range and optional filters (client and/or matter) When they request a receipts bundle export Then the system compiles all receipts whose timestamp_utc falls within the specified range and filters And produces a single downloadable archive containing receipts grouped by record and a top-level manifest with filter_params, record_count, receipt_count, bundle_hash, generation_timestamp And the archive is signed; its signature validates against the current public key And the archive is available within 120 seconds for up to 100,000 receipts or an async job is created with progress and user notification upon completion
One-Click Verification Report Generation
Given a user is on a client, matter, or date-range view When they click Generate Verification Report Then the system verifies chain continuity (previous_hash linkage) and signature validity for all included commits And the report summarizes totals, passed, failed, gaps, duplicates, out-of-order timestamps, and missing previous_hash links And the report provides per-record pass/fail with first failure cause and location (record_id, commit_id) And the report is available as an on-screen view and exportable as PDF and machine-readable JSON And generation completes within 60 seconds for up to 10,000 receipts or runs async with progress and notification
Verification Report Content Redaction
Given records contain protected content (notes, file bytes, titles) When a verification report is generated or exported (PDF or JSON) Then the outputs contain no protected content or plaintext derivatives; only metadata fingerprints and verification outcomes And the JSON contains only allowed fields: scope, generated_at, filters, summary_counts, anomalies, per_record[record_id, status, first_failure, receipt_range, key_versions], per_commit[commit_id, timestamp_utc, content_hash, previous_hash, signature_valid, tamper_flag] And automated scans for seeded PHI/PII strings in the outputs return zero matches
Tamper Detection Alert and Auto-Verification
Given an integrity anomaly occurs (e.g., invalid signature, hash mismatch, missing receipt link, out-of-order timestamp) When the system detects the anomaly during commit, background verification, or user-initiated verification Then an alert is sent to account owners and security contacts within 60 seconds containing summary, severity, and a link to the verification report And an audit log entry is recorded with anomaly type, affected record_id/commit_id, detector, and detection time And an auto-generated verification report for the impacted scope is accessible from the alert
Key Versioning and Signature Validation
Given cryptographic keys are rotated and key_version increments When new receipts are created after rotation Then receipts include the new key_version and validate against the corresponding public key And existing receipts continue to validate using their original key_version And the verification report lists observed key_versions with counts per version And a public key endpoint (e.g., JWKS) exposes active and retired public keys by key_version; unavailable keys yield clear "unable to verify" statuses without errors
Tamper Detection Alerts & Health Monitoring
"As an account owner, I want real-time tamper alerts and a health dashboard so that I can respond immediately to issues and maintain trust with clients and insurers."
Description

Continuously verify integrity chains and storage controls via scheduled jobs and real-time hooks. Detect and surface anomalies such as hash mismatches, missing predecessors, out-of-retention attempts, clock drift beyond threshold, or storage policy downgrades. Trigger configurable alerts (in-app, email, webhook) with severity levels, include a concise incident summary and remediation steps, and automatically preserve a signed incident record. Provide a monitoring dashboard with verification status, recent alerts, and SLA metrics to support operational readiness.

Acceptance Criteria
Scheduled Chain Verification Flags Hash Mismatch
Given a tenant with WORM Sentinel enabled and a scheduled integrity verification job configured And a record in the chain whose stored hash does not match the recomputed hash When the next scheduled verification job runs Then the job marks the verification as failed for the affected chain segment And creates a Critical-severity incident with a concise summary including artifact ID, chain segment, expected hash, actual hash, detection time, and detection source And attaches remediation steps describing quarantine, re-derivation, and contact path And persists a signed, append-only incident record with a cryptographic fingerprint and receipt And dispatches alerts via in-app, email, and webhook to the configured recipients within 60 seconds of detection And the webhook delivery returns 2xx or is retried with exponential backoff for up to 15 minutes with outcome logged And the Monitoring Dashboard updates the Recent Alerts list and the Verification Status to reflect the failure within 60 seconds
Real-Time Tamper Attempt: Out-of-Retention Delete/Overwrite
Given an artifact under active retention that is WORM-protected And alert channels for High severity are configured for in-app and webhook When a user or system attempts to delete or overwrite the artifact before retention expiry Then the platform blocks the operation And generates a High-severity alert including actor identity, attempted action, artifact ID, retention policy details, and detection time And includes clear remediation steps indicating policy constraints and allowed processes for corrections via addendum And persists a signed incident record And sends an in-app alert immediately and a webhook within 5 seconds containing the incident payload And no email is sent for this event per the configured channels
Chain Discontinuity: Missing Predecessor Detected
Given a chain where a referenced predecessor is missing or inaccessible during verification When the integrity verification job evaluates the chain Then the system flags the chain as Unhealthy and generates a Major-severity incident And the incident summary lists the first affected node ID, expected predecessor ID, lookup path attempted, and correlation ID And remediation steps include storage reconciliation and restore procedures And a signed incident record is preserved And the Monitoring Dashboard shows the affected chain count and provides a link to a downloadable verification report for the job run
Clock Drift Beyond Threshold Detected
Given the clock drift threshold is configured to 2 seconds against the tenant’s NTP source When any application node or signing service clock drifts more than the configured threshold Then a Medium-severity alert is generated including component ID, observed drift in milliseconds, threshold, detection time, and NTP source And remediation steps instruct immediate time synchronization and verification rerun for impacted windows And a signed incident record is preserved And alerts are delivered to the configured channels within 60 seconds And the Monitoring Dashboard displays current drift status and time of last successful sync
Storage Policy Downgrade/Immutability Weakening Detected
Given baseline storage immutability and retention policies are registered for the tenant When a monitored bucket/container or vault exhibits a policy change that reduces retention duration or disables WORM/immutability Then a Critical-severity alert is generated including scope (account/container/path), before/after policy values, change initiator if available, and detection time And remediation steps direct immediate rollback and verification of policy enforcement And a signed incident record is preserved And alerts are dispatched to in-app, email, and webhook within 60 seconds And the Monitoring Dashboard highlights a Policy Risk banner until the baseline is restored and re-verified
Monitoring Dashboard: Verification Status, Recent Alerts, and SLA Metrics
Given the tenant has active verification jobs and alerting enabled When an operator opens the Monitoring Dashboard Then the dashboard displays - Current verification job schedule, last run time, last result, and next run ETA per job - Pass/Fail counts and success rate for the last 24 hours and 7 days - Mean Time To Detect (MTTD) for the last 30 days - Alert delivery latency p95 for in-app, email, and webhook for the last 7 days - Recent Alerts feed (last 100) with filters by severity, category, time window, and status - Overall Chain Health with counts of Healthy/Unhealthy chains and drill-down to verification reports And the dashboard supports CSV export of alerts and a download of the latest signed verification report And access is restricted to users with Admin or Compliance roles
Configurable Alert Routing and Severity Policy
Given an Admin configures alert policies mapping anomaly types to severities and delivery channels (in-app, email, webhook) When the Admin updates the policy to disable email for hash mismatches and enable webhook for storage downgrades Then the changes are validated and saved with a new policy version and audit entry And take effect for new incidents within 60 seconds And a synthetic test event confirms the new routing: no email for hash mismatches; webhook sent for storage downgrade And policy export/import endpoints preserve mappings and severities And reverting to the previous policy version restores the prior routing behavior
Read-Only Exports with Public Verification
"As a user sharing records externally, I want read-only exports with built-in verification so that recipients can independently validate authenticity without access to my account."
Description

Enable export of read-only evidence bundles containing selected records, their receipts, and a signed manifest with checksums. Package notes as PDFs with visible hash and timestamp watermarks; include original binary attachments and a manifest.json describing each artifact, its hash, and chain position. Provide expiring, access-controlled download links and optional password protection. Offer a simple public verifier (web endpoint or CLI instructions) to validate the bundle without SoloPilot access, facilitating external reviews by auditors or insurers.

Acceptance Criteria
Bundle Content and Manifest Integrity
Given an authorized user selects one or more records and initiates an export When the export completes and the bundle is downloaded Then the bundle is a single .zip containing: note PDFs, original binary attachments, manifest.json at the root, and a detached signature file for the manifest And each note is exported as a PDF (one per note) And each attachment is included in its original binary format with original filename preserved And manifest.json lists every artifact with: id, filename, size_bytes, mime_type, sha256, chain_position, receipt_id (or receipt_hash), created_at (RFC 3339) And verifying the manifest signature against SoloPilot’s published public key succeeds And recomputing SHA-256 for each artifact exactly matches the sha256 recorded in manifest.json
PDF Notes with Visible Hash and Timestamp Watermarks
Given a finalized note is included in an export When its PDF is opened Then a visible watermark is present on each page showing the note content SHA-256 (hex) and the UTC timestamp in RFC 3339 And the displayed hash equals the sha256 value for the note recorded in manifest.json And the displayed timestamp matches the note’s finalized time recorded in manifest.json And the PDF is marked read-only (no editing permitted in standard viewers)
Expiring, Access-Controlled Download Links
Given a user with export permission generates a download link with an expiry time T and optional access scope When an unauthenticated or unauthorized requester attempts to use the link before expiry Then the request is denied with 401/403 and no bundle bytes are returned When an authorized requester uses the link before expiry Then the bundle download starts successfully When the link is used after expiry or after manual revocation Then the request is denied with 410/403 and no bundle bytes are returned
Optional Password Protection on Download
Given the exporter enables password protection for the bundle When the download link is accessed Then the system requires the correct password before returning any bundle content And submitting an incorrect password denies access without revealing bundle contents And the downloaded archive itself is password-protected so its contents cannot be opened without the same password
Public Verifier — Web Endpoint
Given an external reviewer without a SoloPilot account accesses the public verification endpoint When they upload an exported bundle Then the verifier validates the manifest signature against the published SoloPilot public key And recalculates each artifact’s sha256 and compares to manifest.json And validates each artifact’s chain_position and receipt against the WORM Sentinel receipt included in the bundle And returns a machine-readable JSON report with overall_status = "pass" or "fail" and per-artifact results And the endpoint requires no authentication to perform verification
Public Verifier — Offline/CLI Validation
Given the bundle and SoloPilot’s published public key When the reviewer follows the provided CLI instructions Then the detached signature for manifest.json verifies successfully using standard tools And each artifact’s SHA-256 computed locally matches the value in manifest.json And the resulting pass/fail outcome matches the web verifier’s overall result And the instructions work on macOS, Windows, and Linux environments
Immutability Check — Tampered Bundle Fails Verification
Given an exported bundle is modified after download (e.g., a PDF is altered or manifest.json is changed) When the bundle is verified via the public web verifier or the CLI steps Then the verification returns overall_status = "fail" And the report identifies the exact mismatched artifacts and which checks failed (signature and/or checksum) And if manifest.json is altered, the manifest signature verification fails
Trusted Time-Stamping & Key Management
"As a security-conscious user, I want signatures backed by managed keys and trusted timestamps so that integrity proofs remain verifiable over time."
Description

Manage cryptographic keys and trusted time sources for signing and timestamping receipts and manifests. Use cloud KMS with hardware-backed keys, enforce least-privilege access, and implement periodic key rotation with versioned signatures and revocation tracking. Integrate with a trusted timestamp authority (RFC 3161 or equivalent) and maintain robust NTP synchronization with drift alarms. Persist key metadata with each signature to support long-term validation, and document contingency procedures for disaster recovery and key compromise.

Acceptance Criteria
HSM-Backed Key Generation and Storage in Cloud KMS
Given a request to create a signing key in SoloPilot When the platform provisions the key via the cloud KMS Then the key is generated and stored in an HSM and is non-exportable And the key algorithm is in {ECDSA P-256, RSA-3072} and recorded with key ID, creation time, and rotation policy ID And an audit log entry is written including actor, request ID, result, and HSM-enforced=true And any attempt to export or wrap the private key is denied with a 403 and is auditable
Least-Privilege Access and Separation of Duties for Signing Keys
Given KMS IAM policies are configured When a principal without the signer role calls the sign operation Then the request is denied with error code AccessDenied and is logged with principal ID and action And only the service principal WORM-Signer can perform sign operations; only Security-Admin can rotate/disable keys; no single principal holds both roles And all key-management actions require MFA and just-in-time elevation scoped to <= 60 minutes And a quarterly access review export shows no principals outside the approved allowlist
Key Rotation and Versioned Signatures
Given an active signing key with version vN When a scheduled rotation at 90-day intervals or a manual rotation occurs Then a new key version vN+1 is created and set as primary within 5 minutes And all new signatures reference keyVersion=vN+1 while existing signatures remain verifiable with vN And each signature record persists keyId and keyVersion used at signing time And disabling/revoking vN prevents future signing with vN but does not break verification of artifacts signed before the revocation timestamp And an immutable rotation event is recorded with oldVersion, newVersion, effectiveAt, and actor
RFC 3161 Trusted Timestamp Authority Integration
Given a receipt or manifest is produced When the system requests a timestamp from the configured TSA Then a valid RFC 3161 TST is attached using an allowed hash algorithm in {SHA-256, SHA-384} And the TST verifies against the TSA chain, includes a nonce, serial number, and the configured policy OID And if the primary TSA is unreachable for up to 60s, retries with exponential backoff; after 60s failover to a secondary TSA; if both fail, the item is queued as Timestamp Pending and an alert is sent And the TST and TSA cert chain fingerprints are stored alongside the signature for future verification
Time Synchronization and Drift Alarms
Given NTP is enabled on signing infrastructure When monitoring samples clock offset from at least 3 NTP peers Then absolute offset must be <= 250 ms during normal operation And if offset > 500 ms for more than 60 seconds, a critical alert is emitted and signing operations pause until offset returns to <= 250 ms And drift and pause/resume events are logged with timestamps and peer status And an operational dashboard displays current offset, last sync time, and active peers
Signature Metadata Persistence for Long-Term Validation
Given a note, upload, or edit is signed and timestamped When the record is persisted Then the following immutable metadata fields are stored: keyId, keyVersion, signatureAlgorithm, digestAlgorithm, TSA policy OID, TST serial, TST nonce, TSA chain fingerprints (leaf and intermediates), signing profile version, and signing/time values And metadata is append-only; any correction creates a new addendum with its own signature and cross-reference to the prior record And offline verification using only stored metadata and cached certificates successfully validates 100% of sampled signatures And retention for signature metadata is enforced for >= 7 years
Revocation Tracking and Compromise/DR Procedures
Given external TSA certificates and any internal trust anchors When CRLs/OCSP responses are fetched Then they are refreshed at least every 4 hours; if stale > 8 hours, an alert is raised And upon declaration of signing key compromise, the affected key is disabled immediately, a new key version is promoted within 5 minutes, and all new signatures use the new key And verification results annotate artifacts signed within the compromise window without retroactively invalidating signatures that were valid at signing time And disaster recovery procedures enable restoration of signing capability (including key metadata and trust anchors) within an RTO of 15 minutes and RPO of 5 minutes, with quarterly exercise evidence stored

Binder Composer

Assemble an audit‑ready, time‑stamped binder in one click—notes, consents, policies, session history, and invoices, automatically indexed with a table of contents. Choose templates (HIPAA review, GDPR request, insurance audit, dispute pack) and get a clean PDF/ZIP with manifests and receipts. Saves hours of gathering and formatting while reducing audit anxiety.

Requirements

One-Click Binder Assembly
"As a solo practitioner, I want to assemble an audit-ready binder from a client record in one click so that I can respond to requests quickly without manual gathering."
Description

Generate a complete, audit-ready binder from a client record with a single action. The flow pulls notes, consents, policies, session history, invoices, and attachments across SoloPilot modules, applies chosen template logic, and assembles content in the correct order. It adds a table of contents, section bookmarks, headers/footers, page numbers, client/provider identifiers, and per-document timestamps. It deduplicates documents, labels versions, validates completeness (e.g., missing consents), and produces a manifest of included items. The binder artifact is saved back to the client workspace for reuse and tracked in activity history, ensuring speed, consistency, and traceability while eliminating manual collation.

Acceptance Criteria
One-Click Binder Generation from Client Record
Given a user with Binder Composer permission viewing a client record with at least one eligible item When the user clicks "Assemble Binder" and selects a template (HIPAA review, GDPR request, insurance audit, or dispute pack) Then a binder job is created with a unique ID and a progress indicator appears within 2 seconds And for inputs ≤500 pages and ≤200 items, the binder completes in ≤60 seconds at the 95th percentile And for inputs ≤2,000 pages, the binder completes in ≤3 minutes at the 95th percentile And upon completion, the user can download a single PDF and a ZIP that includes the PDF plus manifest and receipts And on failure, the user sees an error message with cause, a correlation ID, and a Retry action that reuses the same parameters
Template Application and Content Ordering
Given a selected template When the binder is generated Then included sections match the template’s configured modules and date range/redaction rules And the global ordering is: Cover, Table of Contents, Client/Provider Identifiers, Consents, Policies, Notes, Session History, Invoices, Attachments, Receipts, Manifest And items within each section are sorted by their effective timestamp ascending unless overridden by the template And the table of contents entries resolve to the correct page numbers and bookmarks for every section and document
Deduplication and Version Labeling
Given multiple items with identical content or duplicate file IDs across modules When the binder is assembled Then only one instance per unique content is included unless the template explicitly requires all versions And documents with versions are labeled (v1, v2, …) alongside their effective timestamp (ISO 8601) in the section heading And the manifest reflects which versions were included and excludes exact duplicates, preserving traceability
Completeness Validation and Missing Consents Handling
Given the client’s required consent and policy checklist When binder assembly is initiated Then the system validates completeness and expiration status before finalizing And if any required items are missing or expired, the user is prompted to Proceed or Cancel And if Proceed is chosen, the binder includes a "Completeness Check" page listing each missing/expired item with type and date And the completion summary displays counts for total items included, items deduplicated, and missing/expired items
Metadata, TOC, Bookmarks, and Pagination
Given binder generation completes When the resulting PDF is opened Then it contains a clickable table of contents with accurate page numbers for each top-level section and document And PDF bookmarks are present for each top-level section and document And headers/footers on all pages include client full name and ID, provider name and identifier (if configured), binder ID, and "Page X of Y" And each included document displays its own timestamp (ISO 8601 with timezone) on the first page of its section
Manifest and Traceable Source Details
Given the generated binder When the manifest is viewed Then each item lists source module (Notes, Consents, Policies, Session History, Invoices, Attachments), document title, version, created and last-modified timestamps, author, page range in binder, and a SHA-256 content hash And the manifest includes binder-level metadata: template name, generation timestamp, generator user, client ID, SoloPilot app version And the manifest is appended as the final section of the PDF and is available as a standalone machine-readable file in the ZIP
Saving Binder Artifact and Activity History Tracking
Given successful binder generation When saving completes Then a Binder artifact is stored in the client’s workspace under Binders with a unique name "Binder - {Template} - {Client} - {YYYY-MM-DD HHmm}" and metadata (binder ID, template, generated by, generated at, page count, hash) And previous binders are preserved; re-running the same template on the same day appends an incrementing suffix (-1, -2, …) And an activity history entry records who performed the action, when, the template used, counts for items included, deduplicated items, missing/expired items, and a link to the saved binder And users without permission cannot view the binder artifact or activity entry (returns 403)
Template Library & Editor
"As a practitioner, I want to choose and customize binder templates so that each request type includes exactly the required sections and formatting."
Description

Provide a built-in library of binder templates (HIPAA review, GDPR request, insurance audit, dispute pack) and an editor to create, version, and publish custom templates. Templates define sections, data sources (notes, consents, invoices, sessions), filters (date range, tags, case IDs), ordering, cover pages, disclaimers, and formatting. Include variable placeholders, localization, and redaction rules per section. Support draft/publish workflows, rollback to prior versions, role-restricted access, and import/export of templates, ensuring repeatable, compliant output tailored to each request type.

Acceptance Criteria
Built-In Template Library Availability
Given a new SoloPilot account, When the user opens Template Library, Then the library lists HIPAA review, GDPR request, insurance audit, and dispute pack as built-in templates. And Then each built-in template displays name, description, version, last updated, and read-only badge. And When the user selects a built-in template, Then Edit is disabled and Duplicate is enabled. And When Duplicate is clicked, Then a new Draft copy is created under My Templates with owner set to the user.
Custom Template Creation with Sections, Data Sources, Filters, Ordering, Cover, Disclaimers, and Formatting
Given the user clicks New Template, When they enter a name and save, Then a Draft template is created with unique ID and version 0. When the user adds a section of type notes/consents/invoices/sessions, Then the editor requires a data source selection and saves it. And When the user configures filters including date range, tags, and case IDs, Then the system validates values and persists them. And When the user sets section ordering, cover page, disclaimers, and formatting (fonts, margins, header/footer), Then these settings are saved and reflected in preview. And Then Save is blocked with inline errors if no sections exist or any section lacks a data source.
Variable Placeholders and Localization Validation
Given the template includes placeholders (e.g., {{client.name}}, {{invoice.total}}), When Validate is clicked, Then unresolved or invalid placeholders are listed with section and location. And Then validation fails if a placeholder path is unknown for the selected data source or type-mismatched. And Given a default locale is selected, When previewing, Then dates, numbers, and currency are formatted per locale. And When required localized strings for cover, disclaimers, and section titles are missing in the default locale, Then Publish is disabled and missing keys are reported. And When sample data is applied, Then all placeholders render with sample values without errors.
Redaction Rules per Section and Output Consistency
Given the user defines redaction rules for a section (mask fields, remove tagged content), When previewing that section, Then redacted content is masked or removed per rule. And Then redaction rules affect only targeted fields/sections and leave others unchanged. And When a binder is generated using this published template, Then the same redactions are applied in the output PDF/ZIP. And When a rule references a non-existent field or tag, Then validation shows a specific error and prevents Publish.
Draft, Publish, Versioning, Rollback, and Role-Restricted Approvals
Given roles Admin, Editor, Viewer exist, When an Editor submits a Draft for publish, Then an approval request is created and requires Admin approval. And When an Admin approves, Then the template is published as an immutable version (e.g., v1.0) with timestamp and publisher recorded. And When changes are made post-publish, Then a new Draft (e.g., v1.1) is created without modifying the published version. And When Rollback to vN is executed by an Admin, Then a new Draft from vN is created and the rollback event is logged. And When a user without permission attempts edit/publish/delete, Then the action is blocked with a descriptive error and audit log entry.
Template Import/Export with Integrity and Compatibility
Given a valid exported template file (JSON/ZIP with schema version), When importing, Then the system validates checksum, schema version, and required fields before creating a Draft. And When schema versions are incompatible, Then import fails with a clear message and remediation guidance. And When exporting a template, Then the file includes template JSON, localization bundles, and manifest with version, checksum, and created timestamp. And Then import preserves role permissions as defaults unset and does not auto-publish. And Then an import/export action is recorded in the audit log with actor, timestamp, and outcome.
Automated Redaction & PII Masking
"As a compliance-conscious provider, I want automatic redaction and masking rules applied so that I only disclose the minimum necessary information."
Description

Apply configurable, template-scoped redaction and masking rules to ensure minimum-necessary disclosure. Support field-based redaction (e.g., SSN, DOB, contact info), pattern detection (emails, card tokens), and section-level exclusions. Offer a pre-issue review screen for manual markups and overrides with required justification. Watermark redacted pages and record a redaction log referencing each hidden element. Preserve unredacted source files securely while exporting only sanctioned content, reducing risk and effort during audits and data requests.

Acceptance Criteria
Template-Scoped Redaction on Binder Export
Given I select a specific binder template (e.g., HIPAA Review), When I export the binder, Then only the redaction/masking rules assigned to that template are applied and rules from other templates are not applied. Given two templates have different rules for DOB visibility, When I switch templates before export, Then the preview and final export reflect the new template's DOB rule within 2 seconds. Given no template is selected, When I attempt to export, Then I am prompted to select a template and export is blocked until a template is chosen.
Field-Based PII Redaction for Structured Data
Given client records include SSN, DOB, phone, and email fields, When exporting with rules that redact SSN and mask DOB to YYYY-XX-XX, Then all SSN fields are fully removed/burned-in redacted and DOBs are masked across all binder sections and indexes. Given invoices and session notes are included, When field-based rules are enabled, Then redaction is applied consistently across all document types and the table of contents does not expose redacted values. Given SSN is redacted, When searching the resulting PDF text layer and metadata, Then the SSN value is not present or recoverable.
Pattern Detection for Unstructured PII
Given free-text notes contain email addresses, SSNs, and payment tokens (e.g., tok_*), When pattern detection is enabled for these types, Then all occurrences matching the configured patterns are masked according to their rule and non-matching text remains unaltered. Given an exclusion list is configured (e.g., noreply@example.com), When exporting, Then excluded entries are not redacted and are listed in the redaction log as exclusions. Given emails are configured to mask as u***@domain.com, When exporting, Then the mask format is applied uniformly across all occurrences including cross-references and indices.
Section-Level Exclusions from Binder
Given I deselect the Session Notes section via section-level exclusions, When exporting, Then the binder omits that section and the table of contents renumbers pages accordingly. Given sections are excluded, When generating the manifest, Then the manifest lists excluded sections and the configured reason (rule ID or user note) if provided. Given a ZIP export is generated, When inspecting the archive, Then omitted section files are not present anywhere in the package.
Pre-Issue Review with Manual Markups and Overrides
Given the pre-issue review screen is open, When I draw a manual redaction on a page and enter a justification of at least 10 characters, Then the markup saves, the preview updates within 1 second, and the justification is recorded in the redaction log. Given I attempt to save a manual redaction without a justification, When I click Save, Then the action is blocked with a validation error requiring justification. Given an auto-redaction hides a field, When I override to reveal it, Then a required justification is captured with user and timestamp, the change is tracked in the log, and the export reflects the override.
Watermarking and Redaction Log
Given any page contains at least one redaction or mask, When exported, Then that page displays a semi-transparent "REDACTED" watermark that is visible at 100% zoom and is non-selectable/non-removable in the PDF. Given a binder export completes, When reviewing the redaction log, Then each entry includes timestamp, user, rule ID/type (field/pattern/manual), document/section, page number, and justification if manual or override. Given a ZIP export is generated, When inspecting the contents, Then the redaction log is included as JSON and CSV and is referenced in the binder manifest.
Secure Preservation of Unredacted Sources
Given source documents contain PII, When export completes, Then only redacted/masked derivatives are included in the output and no unredacted source files are embedded, linked, or recoverable. Given I open the exported PDF, When attempting to copy, OCR, or inspect PDF objects where redactions were applied, Then underlying text is not recoverable and is replaced with vector/image fill. Given originals are stored in SoloPilot, When viewing in-app, Then originals remain unchanged and access is restricted to authorized roles with access events logged.
Immutable Timestamps, Hashing, and Receipts
"As a provider facing an audit, I want binders to include immutable timestamps and verification receipts so that I can prove integrity and authenticity of the documents."
Description

Embed verifiable proof of integrity and timing into every binder. Generate server-signed timestamps, compute SHA-256 hashes for each included file and for the full binder, and output a human-readable receipt plus a machine-readable manifest (CSV/JSON). Store hashes and metadata (creator identity, client ID, template, timezone) to support later verification. Include a QR code or link on the receipt to a verification endpoint that confirms the binder remains unaltered, increasing credibility and audit readiness.

Acceptance Criteria
Server-Signed Timestamp on Binder Export
Given a user exports a binder via Binder Composer When the export completes Then a server-signed timestamp is generated and embedded in both the receipt and manifest And the timestamp is formatted as ISO 8601 with timezone offset (e.g., 2025-09-22T14:05:30.123-04:00) And a digital signature covering (binder_id + binder_sha256 + timestamp) is produced using the server private key And the receipt displays the signature (Base64) and signing key_id And the signature verifies successfully using the published public key at /keys/current And the timestamp recorded is within 2 seconds of server time at export completion
Per-File and Whole-Binder SHA-256 Hashing
Given a binder that includes multiple files (notes, consents, policies, session history, invoices) When the binder export completes Then a SHA-256 hash is computed for each included file and for the final binder artifact (PDF or ZIP) And manifest.json and manifest.csv list each file with path, type, size_bytes, and sha256 (lowercase hex, 64 chars) And the receipt displays the binder_sha256 (lowercase hex, 64 chars) And recomputing hashes from the downloaded binder matches all values in the manifest and receipt
Human-Readable Receipt with QR Verification Link
Given the binder export completes When the receipt is generated Then it contains: binder_id (UUID v4), created_at (ISO 8601 with timezone), creator_user (full name and user_id), client_id, template_name, timezone, total_file_count, binder_sha256, signature key_id, and verification URL And the receipt includes a QR code that encodes the verification URL exactly And scanning the QR code with default iOS and Android camera apps opens the verification URL successfully And visiting the URL returns HTTP 200 for a known binder_id and shows the same metadata and binder_sha256 as on the receipt
Machine-Readable Manifest in CSV and JSON
Given the binder export completes When the binder files are assembled Then two files exist at the binder root: manifest.json and manifest.csv And manifest.json contains: binder_id, created_at, timezone, creator {id, name}, client_id, template_name, binder_sha256, and files[] with {index, name, path, type, size_bytes, sha256, last_modified_iso8601} And manifest.csv has columns exactly: index,name,path,type,size_bytes,sha256,last_modified_iso8601 and a row for every included file And the number of files[] entries equals the total_file_count printed on the receipt
Persist and Retrieve Hashes and Metadata
Given a binder export completes When the system stores the record Then the database record includes: binder_id, created_at, timezone, creator_user_id, client_id, template_name/id, binder_sha256, per-file sha256 list, signature value, signing key_id And the stored values exactly match the receipt and manifest And the record is retrievable via internal API after a server restart and after 7 days in staging And attempts to modify binder_sha256 or signature via public APIs are blocked and return HTTP 403
Verification Endpoint: Integrity Check Pass/Fail
Given a user opens the verification URL from the receipt When no file or hash is submitted Then the page displays stored binder metadata, stored binder_sha256, and signature verification status (Valid/Invalid) When the user uploads the binder file or submits a SHA-256 hash Then the endpoint compares the submitted value to the stored binder_sha256 and returns PASS if equal, otherwise FAIL And a human-readable page is shown by default, with a JSON response available at ?format=json containing {binder_id, result, reason, binder_sha256_stored, binder_sha256_submitted, signature_valid} And responses use HTTP 200 for PASS, HTTP 422 for FAIL, and HTTP 400 for malformed input And altering a single byte of the binder results in FAIL with reason "Hash mismatch"
Export & Delivery Options (PDF/ZIP)
"As a busy freelancer, I want to export binders as PDF or ZIP and deliver them securely so that recipients can access them easily while maintaining confidentiality."
Description

Offer export as a consolidated PDF with table of contents, bookmarks, and page numbers, and/or a ZIP containing source documents plus the manifest and receipt. Support PDF/A compliance, font embedding, and document permissions (print/copy restrictions). Enable password-protected encryption, expiring download links, secure in-app sharing, and optional delivery to connected cloud drives. Handle large binders with streaming generation, chunked transfers, resumable downloads, and retries to ensure reliable, secure delivery.

Acceptance Criteria
Consolidated PDF export with TOC, bookmarks, and page numbers
Given a binder containing notes, consents, policies, session history, and invoices When the user selects "Export as PDF (consolidated)" Then a single PDF is generated that contains all binder contents in the binder's index order And the PDF includes a table of contents where each entry links to the corresponding section via bookmark And each page displays a sequential page number in the footer And each section start has a named PDF bookmark matching the section title And the table of contents page numbers exactly match the actual section start pages And the PDF opens without errors in Adobe Acrobat, Apple Preview, and Chrome PDF viewer
ZIP export with source documents, manifest, and receipt
Given the user selects "Export as ZIP" Then the system produces a ZIP that contains each original source document as a separate file without reformatting And the ZIP includes a manifest.json listing each file path, byte size, SHA-256 checksum, and source type And the ZIP includes a receipt file (PDF or TXT) containing export timestamp, binder id, requester, total file count, and total size And the folder structure reflects the binder's section hierarchy And all file and folder names are ASCII-safe and OS-compatible (Windows/macOS) with no name longer than 240 characters And the ZIP passes integrity check (e.g., unzip -t) with no errors
PDF/A compliance, font embedding, and permission controls
Given the user enables "PDF/A compliance" When exporting a consolidated PDF Then the output validates as PDF/A-2b using an industry-standard validator (e.g., veraPDF) with zero errors And all fonts used are embedded (subset or full) with no external font dependencies Given the user configures document permissions to disallow copy and allow printing When the PDF is opened in a standard viewer (e.g., Adobe Acrobat) Then the document security summary shows Content Copying: Not Allowed and Printing: Allowed And attempts to copy text are blocked while printing is permitted
Password-protected encryption for exports
Given the user enables "Password-protect export" and enters a password meeting policy (min 12 chars, at least 1 letter and 1 number) When exporting as PDF or ZIP Then the exported file is encrypted (PDF: AES-256; ZIP: AES-256) and requires the password to open And opening with an incorrect password fails with an authentication error And opening with the correct password succeeds And the decrypted file checksum matches the checksum of the same binder exported without encryption And the password value is not stored in logs, analytics, or included in emails/notifications
Expiring download links and secure in-app sharing
Given a completed export When the user generates a download link with expiry set to 7 days Then the link returns HTTP 200 and serves the file until the exact expiry timestamp And after expiry, the link returns HTTP 403 or 410 and no file bytes are sent And the user can manually revoke the link, after which access returns HTTP 403 within 60 seconds Given the user shares the export in-app with a specific SoloPilot user and role When the recipient is authenticated and has the shared permission Then the recipient can view and download within the app, while non-authorized users receive HTTP 403 And all link creations, downloads, revocations, and share accesses are recorded in an immutable audit log with timestamp, actor, and IP
Delivery to connected cloud drives
Given the user has connected Google Drive, Dropbox, or OneDrive with valid OAuth scopes When the user selects "Deliver to cloud drives" and chooses a target folder Then the export file is uploaded using the provider's resumable upload API And on success, a confirmation shows provider, path, file size, and file version id And on quota exceeded, revoked token, or network failure, the system retries with exponential backoff up to 5 attempts and surfaces a clear error with remediation steps And no duplicate files are created; subsequent deliveries to the same path either overwrite when overwrite is selected or create a new version when versioning is enabled
Large binder handling with streaming, chunking, resumable downloads, and retries
Given a binder with at least 5,000 pages or total raw size >= 2 GB When the user exports as PDF or ZIP Then generation uses streaming such that server peak memory usage does not exceed 1.5x the largest single source file And the download begins streaming within 5 seconds of request initiation And HTTP Range requests are supported, and interrupted downloads can be resumed within 24 hours using the same URL And the client can successfully complete the download after at least two simulated network interruptions without re-downloading completed chunks And the final file checksum (SHA-256) matches the value provided in the manifest or API response And transient server or network errors trigger automatic retries with exponential backoff up to 6 attempts
Permissions and Consent Enforcement
"As an account owner, I want binder generation to respect permissions and consent checks so that only authorized, lawful disclosures are made."
Description

Enforce role-based access and client scoping for binder creation, preview, and distribution. Verify requisite consents before including protected materials; require documented justification for overrides and capture approver identity when approvals are needed. Log all access and downloads, apply rate limits, and ensure tenant isolation. Provide admin controls to define who can create, edit templates, and share binders, ensuring lawful, least-privilege disclosures that align with organizational and regulatory policies.

Acceptance Criteria
Permissions Configuration and Enforcement for Binder Operations
Given an admin assigns Binder:Create, Binder:Preview, and Binder:Share permissions to Role X and removes them from Role Y When a user with Role X initiates each respective action Then the action succeeds with HTTP 2xx and an audit entry is written for the actor and action Given a user with Role Y attempts Create, Preview, or Share When the request is submitted via UI or API Then the request is denied with HTTP 403 and no binder artifact or share is created Given permission changes are saved When a previously authorized user loses Binder:Share Then within 60 seconds subsequent share attempts return HTTP 403 and share UI controls are disabled on refresh Given a user without Binder:TemplateEdit permission When they attempt to access template editing endpoints or UI Then access is blocked with HTTP 403 and no template changes are persisted
Client Scoping Enforcement
Given a user is scoped to clients C1 and C2 When they list, create, preview, download, or share binders Then only binders for C1 and C2 are returned or allowed and other clients are not visible Given a user attempts to create a binder for an unscoped client C3 When the request is submitted Then the system returns HTTP 403 or HTTP 404 and no binder is created Given a direct URL or ID for a binder belonging to client C3 When the unscoped user attempts to access or download it Then the system returns HTTP 404 and a denied access event is logged
Consent Verification for Protected Materials
Given a binder includes protected materials When generation is requested Then the system validates that each item has an active consent with coverage dates including the item date and excludes any items without valid consent while listing them in a validation report by identifier Given no valid consent exists for any protected items When generation is requested Then the request is blocked with HTTP 422 and a structured error lists the missing consent types and item identifiers and no binder is produced Given protected items are included When the binder manifest is produced Then each included protected item lists consentId and consentVersion in the manifest Given a consent has expired before the material date When generation is requested Then those items are excluded and flagged in the manifest with reason ConsentExpired
Override Workflow with Approval Capture
Given required consent is missing When a user with Override:Request selects Request Override Then a justification text input is required with a minimum of 20 characters before submission is enabled Given an override request is submitted When an approver with Override:Approve reviews it Then approval requires explicit confirmation and captures approver userId, role, timestamp, and decision and upon approval binder generation proceeds and the manifest includes overrideId and approver identity Given an override request is denied When the requester attempts binder generation referencing that override Then generation is blocked with HTTP 403 and the denial reason is displayed Given an override approval specifies clientId, material types, and date range When generation includes items outside that scope Then those items remain blocked and are listed in the validation report as OutOfScopeForOverride
Comprehensive Audit Logging
Given any binder operation including create, preview, download, share, revoke share, and delete draft When the operation completes or is denied Then an audit entry is written with eventType, binderId, clientId, actorUserId, actorRole, timestamp in UTC ISO 8601, IP address, userAgent, outcome, and reasonCode Given audit logs are queried by binderId and date range When an admin views the results Then all corresponding events are returned in chronological order and counts match the number of operations performed during testing Given a user attempts to modify or delete an existing audit entry via API or UI When the request is made Then the system returns HTTP 403 or HTTP 405 and no changes are persisted Given a log integrity check runs When verifying entries Then each entry contains an integrity hash and the sequence validates without gaps or tampering indicators
Rate Limiting of Binder Operations
Given per-user limits of Create 5 per minute, Preview 20 per minute, Download 30 per minute, and Share 10 per minute When a user exceeds a limit Then the API returns HTTP 429 with a Retry-After header and a rate-limit event is logged Given a per-tenant daily limit of Create 1000 per day resetting at 00:00 UTC When the limit is reached Then further create requests return HTTP 429 until reset and no jobs are enqueued Given requests are within limits When actions are performed Then no rate limit errors occur and binder creation enqueue p95 latency is under 2 seconds
Tenant Isolation Across All Binder Operations
Given a user is authenticated under Tenant A When they attempt to access any binder, client, consent, or template resource belonging to Tenant B using direct IDs or shared URLs Then the system returns HTTP 404 and logs a cross-tenant access denial and no data from Tenant B is present in the response Given a binder export is generated for Tenant A When the artifact is stored and later retrieved Then the storage path and encryption keys are namespaced to Tenant A and retrieval with Tenant B credentials fails with HTTP 404 Given a share link is generated for a binder When accessed without valid Tenant A authorization and Binder:Share permission Then access is denied with HTTP 401 or HTTP 403 and no cross-tenant metadata is exposed

Retention Orchestrator

Set defensible retention rules by artifact type (clinical notes, coaching notes, billing, messages) with region‑specific defaults. Legal holds pause deletion with a visible banner and reason capture; scheduled purges issue certificates of destruction. Automates compliance while preventing accidental data hoarding or premature deletes.

Requirements

Retention Rules Builder (Artifact + Region Defaults)
"As a workspace admin operating in multiple regions, I want to define retention rules per artifact type with regional defaults so that our data is kept or destroyed in a compliant and consistent manner without manual cleanup."
Description

Provide a configurable rules engine and UI to define retention durations and deletion behaviors by artifact type (clinical notes, coaching notes, billing records, messages) with region-specific default templates. Support policy scopes at workspace level with optional overrides at client and project/matter levels. Allow selection of retention anchors (e.g., last session date, invoice paid date, note sign-off date, last message activity), grace periods, and exceptions (e.g., minors, high-risk cases). Include versioning of policies with effective-from dates, inline validation (e.g., cannot be shorter than governing regional template), and a readable policy summary. Integrate with SoloPilot’s data model and automations so that session-to-invoice and note workflows automatically tag artifacts with the correct policy and retention clock. Expose an API for programmatic policy management and ensure changes are audited.

Acceptance Criteria
Region Default Application and Minimum Enforcement
Given a workspace in region "EU" When an admin creates a new retention policy using the "EU Default" template Then default durations and deletion behaviors pre-fill for clinical notes, coaching notes, billing records, and messages And when the admin reduces any duration below the template minimum, an inline error "Must be ≥ [min]" is shown and Save is disabled And when all fields meet or exceed minimums, Save is enabled And when saved, the policy persists with region and template identifiers and a human-readable summary for each artifact type is generated
Artifact-Type Anchors, Grace, and Deletion Behavior Configuration
Given the policy editor When the admin sets for Clinical Notes: anchor = Note sign-off date, duration = 7 years, grace = 30 days, deletion behavior = Hard delete Then the rule is saved and the summary reads "Hard delete 7y + 30d after sign-off" When the admin sets for Billing Records: anchor = Invoice paid date, duration = 10 years, grace = 0 days, deletion behavior = Soft delete Then the rule is saved and the summary reads "Soft delete 10y after invoice paid" And if an anchor not supported by a selected artifact type is chosen, an inline validation error prevents saving
Client-Level Override for Minors and High-Risk Exceptions
Given a workspace policy that retains Clinical Notes for 7 years from sign-off and a Minor Exception configured as "retain until age of majority + 3 years, or base retention, whichever is later" When the client is a minor on the anchor date Then the computed retention end date follows the Minor Exception rule When an admin adds a client-level override to increase retention to 10 years Then the override applies to that client's artifacts and cannot be set below the workspace/regional minimum When a client is flagged High-Risk with an additional +2 year exception Then the retention end date reflects the extension in the client policy summary and artifact-level determination
Project-Level Override and Inheritance Precedence
Given a workspace policy with Messages retention = 1 year, a project override = 2 years, and a client override = 3 years When a message is linked to both the client and the project Then the client override applies (3 years) When a message is linked only to the project Then the project override applies (2 years) When an override sets a duration below the workspace or regional minimum Then saving is blocked with an inline error and Save remains disabled And when viewing the rule trace for any message, the system shows the applied rule, its source (client/project/workspace), and evaluated durations
Policy Versioning with Effective-From Dates
Given policy version v1 effective-from 2025-10-01 and version v2 effective-from 2025-12-01 When artifacts have anchor dates on or after 2025-12-01 Then v2 applies When artifacts have anchor dates before 2025-12-01 Then v1 applies When creating a new version Then effective-from is required and cannot be earlier than the current time minus 24 hours And when a new version is saved, prior versions remain immutable and visible in history And when viewing an artifact's retention determination, the applied policy version ID and effective-from date are displayed
Automatic Tagging in Session-to-Invoice and Notes Workflows
Given a completed session that triggers note sign-off and invoice creation When the note is signed Then the Clinical Note is tagged with the applicable policy and retention anchor = note sign-off date When the invoice is marked paid Then the Billing Record retention anchor updates to invoice paid date and the retention end date is recalculated When client or project overrides exist Then those overrides determine the applied policy for tagging When an anchor date changes (e.g., payment date corrected) Then the retention end date is recalculated within 5 minutes and the change is logged
Policy Management API, Validation, and Audit Logging
Given policy management API endpoints When a request POST /policies includes a duration below the regional minimum Then the API responds 422 with field-level validation errors When a request PUT /policies/{id} attempts to set an effective-from in the past Then the API responds 422 and no version is created or modified When a policy or override is created, updated, or deleted via UI or API Then an audit log entry is recorded with actor, action, target, before/after values, timestamp (UTC), and origin (UI/API), and is retrievable by admins And access to API endpoints is authorized according to workspace admin permissions
Legal Hold Management with Banner and Reason Capture
"As a legal/compliance owner, I want to place and manage legal holds with visible indicators and required rationale so that no relevant data is deleted during disputes or audits."
Description

Enable placement and management of legal holds at multiple scopes (workspace-wide, client, project/matter, artifact type, or specific artifacts). Applying a hold must require a reason, optional external reference (case/ticket), approver, and expected review date. While active, holds pause all deletions for affected data and display a visible banner across relevant SoloPilot surfaces (notes editor, client profile, billing, messaging) indicating the hold and a link to details. Support bulk holds, hold expiration/review workflows, and exportable hold rosters. Provide audit logs of hold lifecycle events and API endpoints/webhooks for downstream systems. Ensure holds override retention rules in conflict resolution.

Acceptance Criteria
Workspace-Wide Hold Requires Reason, Approver, and Review Date
- Given a user with Hold:Manage permission is in the Create Legal Hold form, When the user selects Scope = Workspace, Then the Reason field is required, the Approver field is required, and the Expected Review Date must be today or later. - Given required fields are missing or invalid (e.g., past review date), When the user submits, Then the request is rejected with inline validation messages and a 400 response via API. - Given all required fields are valid, When the user submits, Then a hold record is created with a unique ID, scope=workspace, status=Active, and persisted metadata (reason, approver, expectedReviewDate, optional externalReference, createdBy, createdAt). - Given a user without Hold:Manage permission, When they attempt to create a hold via UI or API, Then the action is blocked with a 403 error. - Given an API client calls POST /holds with scope=workspace and valid payload, When processed, Then the API returns 201 with the hold representation and Location header.
Client-Scoped Hold Pauses Deletions and Displays Banners
- Given an Active client-scoped hold for Client C, When any delete or purge action targets artifacts belonging to Client C (notes, billing, messages), Then the operation is blocked server-side with error code 423 Locked and an audit event is recorded. - Given the same hold is Active, When a user views Client C’s notes editor, client profile, billing, or messaging surfaces, Then a visible Legal Hold banner is displayed with reason summary and a “View details” link. - Given accessibility requirements, When the banner renders, Then it meets AA contrast, includes aria-labels, and is dismissible only for the session (does not disable hold enforcement). - Given the hold is Released, When the user refreshes any affected surface, Then the banner is no longer shown and deletion attempts behave per normal retention rules.
Artifact-Type Hold Overrides Retention Rules and Scheduled Purges
- Given an Active hold scoped to Artifact Type = Billing, When the retention job runs and identifies billing items eligible for deletion, Then those items are skipped, logged as “Skipped: Legal Hold,” and no certificate of destruction is generated for them. - Given both a retention rule and a legal hold apply to the same artifact, When conflict resolution occurs, Then the legal hold takes precedence and deletion does not occur. - Given a user attempts manual deletion of a held billing artifact, When the request is sent, Then the API returns 423 Locked with a message referencing the hold ID and scope.
Bulk Hold Creation with Validation and Idempotency
- Given an authorized user uploads a CSV to create holds in bulk (mix of scopes: client, project/matter, artifact type), When processing occurs, Then each row is validated for required fields per scope and invalid rows are rejected with row-level error reasons while valid rows are created. - Given the same CSV (or API payload) is retried with an Idempotency-Key within 24 hours, When processed, Then previously created holds are not duplicated and the response indicates which records were deduplicated. - Given a bulk request exceeds the maximum allowed items (e.g., >10,000), When submitted, Then the request is rejected with a clear error indicating the limit. - Given bulk processing completes, When the user views the summary, Then counts of created, deduplicated, and failed holds are shown and downloadable as a results file.
Hold Review and Expiration Workflow
- Given an Active hold with an Expected Review Date, When it is 14 days before that date, Then the approver and creator receive review reminder notifications (in-app and email if enabled). - Given the Expected Review Date is reached without action, When the system evaluates the hold, Then its state transitions to Needs Review (still enforces deletion pause) and the banner indicates “Review overdue.” - Given an authorized user reviews a hold, When they choose Renew and set a new review date, Then the hold remains Active and the new date is saved and audited. - Given an authorized user releases a hold, When they submit a Release with a required Release Reason, Then the hold status becomes Released, enforcement stops, and the change is recorded in audit logs. - Given multiple holds apply to an artifact, When one hold is released, Then the artifact remains protected until all applicable holds are Released.
Exportable Hold Roster with Filters and Permissions
- Given an authorized user opens the Hold Roster, When they filter by status (Active/Needs Review/Released), scope, approver, date range, or artifact type, Then the list updates accordingly with accurate counts. - Given the roster is visible, When the user clicks Export CSV, Then a CSV is generated containing: holdId, scope, targets, reason, externalReference, approver, status, createdBy, createdAt (ISO 8601 UTC), expectedReviewDate, lastUpdatedAt, affectedArtifactTypes. - Given a user without export permission attempts to export, When they try, Then the export control is disabled in UI and API returns 403. - Given there are >10,000 holds, When exporting, Then the system streams the file without timeouts and includes pagination metadata in the export job record.
Audit Logs, API Endpoints, and Webhooks for Hold Lifecycle
- Given any lifecycle event (create, update, renew, release), When it occurs, Then an immutable audit log entry is recorded with who, when, IP/actor, action, scope, targets, and old→new field values. - Given webhooks are configured, When a lifecycle event occurs, Then a legal_hold.changed webhook is sent within 10 seconds with eventType, hold payload, and signature; on non-2xx responses it retries with exponential backoff up to 24 hours. - Given API clients need integration, When they call GET /holds and GET /holds/{id}, Then the API returns paginated, filterable results and full hold details including scopes and affected targets. - Given API clients need to release a hold, When they call POST /holds/{id}/release with releaseReason, Then the hold is released and a webhook and audit entry are generated.
Scheduled Purge Engine with Certificates of Destruction
"As an administrator, I want automated, reliable purges with verifiable certificates so that we can prove compliant destruction without manual effort."
Description

Implement a scalable, idempotent purge engine that enforces retention policies on a schedule with configurable maintenance windows and time zones. The engine batches deletions, handles retryable failures, and ensures referential integrity (e.g., purging message attachments when messages are purged). Upon completion, generate tamper-evident Certificates of Destruction per job containing policy version, artifact counts/ids, timestamps, executor, region, and hash digests; deliver via email and store as immutable evidence within SoloPilot. Provide progress telemetry, error reporting, and partial job resumption. Coordinate with backup/replication policies to ensure compliant deletion across storage tiers within allowable windows and record completion status. Integrate with billing so that financial records respect statutory minimums before purge.

Acceptance Criteria
Local Timezone Maintenance Window Scheduling
Given org timezone=America/New_York and maintenance_window=01:00–03:00 local on Mondays and eligible artifacts with retention_end <= 2025-09-22T01:15:00-04:00 When the scheduler evaluates jobs at 01:15 local Then a purge job is enqueued and starts within the window and no jobs start outside the window And all job, log, and CoD timestamps include both UTC and local offsets aligned to the policy’s region And on DST transitions, the engine runs at the correct wall-clock time without duplicate or skipped runs
Cascading Deletes Preserve Referential Integrity
Given 100 messages with 150 attachments and batch_size=500 When messages older than the retention threshold are purged Then 100 messages and their 150 attachments are deleted in the same job And no orphan attachments or foreign-key violations remain (DB integrity check passes) And object-store blobs corresponding to deleted attachments are removed And counts by artifact type are recorded in the job summary
Idempotent Batching, Retry, and Checkpoint Resume
Given N=2,000 eligible artifacts, batch_size=500, retry_limit=3, and idempotency keys per artifact When a transient error occurs during batch 2 Then batch 1 deletions are not retried, batch 2 items are retried with exponential backoff up to 3 times, and the job writes a checkpoint before each batch And upon manual rerun, only previously failed items are processed; no artifact is deleted more than once And the final summary reports Deleted=N, Retried<=N, Failed=0
Tamper‑Evident Certificate of Destruction (CoD)
Given a purge job completes with Deleted>0 When generating the CoD Then the CoD contains: job_id, policy_version, region, executor identity, start/end timestamps (ISO 8601, UTC and local), artifact counts by type, list or manifest of artifact IDs, SHA-256 digest of the manifest, and a system signature (HMAC-SHA256 with KID) And the CoD is emailed to configured recipients within 5 minutes and stored immutably with write-once retention >= 7 years And any attempt to modify the stored CoD is blocked; periodic hash verification matches the original digest
Progress Telemetry and Error Reporting
Given a running purge job When querying the jobs API or subscribing to events Then progress fields (percent_complete, batches_total, batches_completed, items_deleted, items_failed, ETA) update at least every 30 seconds And each error record includes code, message, artifact_id, batch_id, retry_count, and correlation_id And when failure_rate exceeds the configured halt_threshold (e.g., 5%), the job transitions to Failed and emits an alert; otherwise it completes with status Succeeded
Cross‑Tier Deletion with Backup/Replication Coordination
Given primary storage, replicas, and backups with configured replication_window=4h and max_backup_window=30d When a purge job deletes eligible artifacts Then deletions propagate to replicas within 4 hours and backup entries are scheduled so no recoverable copies remain beyond 30 days And the job remains in Pending-Backups until all tiers confirm; completion status is recorded only after confirmations And the CoD includes the cross-tier confirmation timestamps and any deferrals with reasons
Billing Retention Guardrails
Given region=US with statutory_min_years=7 and billing records aged < 7 years When a purge job evaluates billing artifacts Then those records are not deleted; the reason "statutory-min-retention" is logged and summarized in the job and CoD And when non-billing artifacts are referenced by retained billing records, their deletion is deferred or redacted per policy without breaking billing referential integrity; deferrals are counted and reported
Impact Preview and Dry‑Run Deletion Simulator
"As a workspace admin, I want to preview what a purge will delete before it runs so that I can verify nothing critical is removed and align stakeholders."
Description

Offer a non-destructive preview mode that simulates retention policy execution for a chosen window, showing counts and lists of artifacts slated for deletion, grouped by type, client, region, and policy. Present risk flags (e.g., items under recent activity or open invoices) and estimated storage reclaimed. Allow CSV/JSON export and one-click conversion of the preview into a scheduled purge after approval. Include safeguards such as time-limited previews, data sampling for very large sets, and diff views across policy versions to visualize change impact.

Acceptance Criteria
Region‑Aware Templates and Conflict Resolution Engine
"As an admin working across regions, I want trustworthy templates and clear conflict rules so that our policies match local laws and I understand why items are retained or deleted."
Description

Ship preloaded, maintainable retention templates reflecting common jurisdictions (e.g., GDPR/EEA, HIPAA/US, state/province variants) with citations and minimums for artifact categories. Automatically propose defaults based on workspace region and client residency, with the ability to opt into stricter policies. Implement a deterministic hierarchy for conflicts (Legal Hold > Explicit Override > Workspace Policy > Regional Template) and provide an explanation UI that shows why a given item is kept or purged. Support automatic updates to templates with changelogs and opt-in review flows before applying changes to existing workspaces.

Acceptance Criteria
Preloaded Jurisdiction Templates Are Installable
Given a new SoloPilot workspace is created with no custom retention policies When the admin opens Retention Orchestrator > Templates Then the system lists preloaded regional templates including at minimum: GDPR/EEA, HIPAA/US baseline, US-CA (California), US-NY (New York), and CA-ON (Ontario) And each template includes artifact categories (clinical notes, coaching notes, billing, messages) with a retention minimum value and legal citation per category And each template displays a template version, last updated date, and source publisher And templates can be previewed without applying to the workspace
Defaults Proposed From Workspace Region And Client Residency
Given workspace region = United States and "Prefer Strictest Applicable Policy" = Off And a client profile is created with residency = Germany (EEA) When the admin opens the client's retention settings Then the proposed defaults are derived from the HIPAA/US baseline template And the UI surfaces an alternative suggestion from GDPR/EEA for categories where GDPR minimums differ And enabling "Prefer Strictest Applicable Policy" updates the proposed defaults to the stricter of HIPAA/US and GDPR/EEA per category And saving applies the selected defaults and records the selection in the audit log
Conflict Resolution Applies Deterministic Hierarchy
Given an artifact with Regional Template minimum = 2 years, Workspace Policy = 3 years, Explicit Override = 1 year, and an active Legal Hold When the retention engine evaluates the purge date Then Legal Hold supersedes all and the outcome is "retain until hold release" And the evaluation trace records the applied precedence order: Legal Hold > Explicit Override > Workspace Policy > Regional Template And when the Legal Hold is released, the Explicit Override of 1 year takes effect and a new purge date is computed from creation date accordingly
Explanation UI Shows Keep/Purge Reasoning
Given a message artifact scheduled to purge on 2030-06-30 due to Workspace Policy of 5 years When a user clicks "Why is this scheduled?" in the artifact details Then the explanation panel shows: decision = purge, effective date = 2030-06-30, applied rule = Workspace Policy (5 years), rule source = Workspace Policy for Messages And the panel lists other considered rules (Regional Template, Explicit Override, Legal Hold) with reasons they did not apply And the panel provides a link to the legal citation(s) for the Regional Template minimum and shows whether "Prefer Strictest Applicable Policy" influenced the decision
Legal Hold Pauses Deletion And Captures Reason
Given a billing artifact that would purge in 14 days When an admin places a Legal Hold with reason "Regulatory inquiry #2025-07" Then the artifact's purge schedule changes to "on hold (no purge)" And a visible banner appears on the artifact and in the legal holds dashboard showing the reason, placer, and date And the action is recorded in the audit log with timestamp, actor, and reason And the explanation UI indicates Legal Hold precedence as the active rule
Template Update Review And Apply Flow
Given the platform publishes GDPR/EEA template update from v1.4 to v1.5 with changed minimums for messages When a workspace admin opens the Updates Center Then a changelog displays the version diff, impacted artifact categories, and citations And the admin can Accept, Defer, or Schedule the update for the workspace And no changes apply to existing workspaces until explicitly accepted And upon acceptance, updated minima are previewed, and the admin must confirm any items that would purge earlier than previously scheduled And the acceptance event, decisions, and user are recorded in the audit log
Explicit Override Creation And Validation
Given a workspace with HIPAA/US baseline applied and "Prefer Strictest Applicable Policy" = On When a user attempts to set an Explicit Override for clinical notes to 2 years where the stricter applicable minimum is 6 years Then the system blocks saving the override and displays a message indicating the minimum allowed is 6 years due to stricter policy enforcement And when the user sets the override to 7 years, the system accepts it and marks the source as Explicit Override And the audit log captures the override creation, previous value, new value, actor, and timestamp
RBAC and Dual‑Control Approvals for Destructive Actions
"As an organization owner, I want role-based controls and two-person approvals so that no single user can accidentally or maliciously delete sensitive data."
Description

Introduce fine-grained roles and permissions for retention operations: who can create/edit policies, place/remove legal holds, run previews, schedule purges, and download certificates. Require dual-control (two-person approval) for high-risk actions such as enabling a new purge policy, reducing retention below a threshold, or executing a purge above a volume limit. Enforce MFA re-prompt and mandatory change justification comments. Support approval via in-app workflow with notification channels (email/Slack) and maintain an auditable approval trail linked to the action.

Acceptance Criteria
Immutable Audit Trail and Evidence Export
"As a compliance officer, I want a complete, tamper-evident audit trail with exportable evidence so that we can satisfy audits and investigations."
Description

Capture an immutable, append-only audit trail for all retention-related events: policy changes, hold lifecycle actions, previews, purge runs, certificate generation, and API access. Use tamper-evident hashing and time-stamping, with optional WORM storage retention. Provide searchable in-app views with filters by user, client, artifact type, and time range, plus export to PDF/CSV/JSON and a streaming API/webhook for SIEM integration. Define a distinct retention policy for audit logs to ensure evidence remains available beyond data deletion while respecting privacy constraints.

Acceptance Criteria
Append-Only Tamper-Evident Audit Log
- Given any retention-related event occurs, When the system records it, Then a new audit record is appended and no existing record is modified or deleted via any UI/API. - Given an actor attempts to update or delete an audit record, When the request is processed, Then the system rejects it with HTTP 403 and logs an "audit.write_blocked" event. - Given an audit record is created, When stored, Then it contains: event_type, actor_id, actor_type, scope (workspace/region), client_id (nullable), artifact_type, target_id, outcome, reason (nullable), request_id, ip (nullable), user_agent (nullable), timestamp_utc (ISO 8601 ms precision), previous_hash, record_hash. - Given a contiguous range of audit records, When hash-chain verification runs, Then verification succeeds; if any record is altered, Then verification fails and identifies the first invalid record index. - Given optional WORM storage is enabled, When an audit record is written, Then it is committed to WORM within 60 seconds and returns a WORM_retention_until timestamp.
Retention Event Coverage Logging
- Given retention policies are created, updated, versioned, or deleted, When the action is saved, Then an audit event (policy.created|policy.updated|policy.versioned|policy.deleted) is recorded with policy_id and effective_at. - Given a legal hold is created, updated, extended, or released, When the action completes, Then an audit event (hold.created|hold.updated|hold.extended|hold.released) is recorded with hold_id, reason, and affected scopes. - Given a purge preview runs, When the preview completes, Then an audit event preview.completed is recorded with candidate_count and filters used; no delete events are logged. - Given a purge run starts and completes, When items are deleted, Then audit events purge.started, item.deleted (per item or batch with counts), and purge.completed are recorded with counts (deleted, skipped, failed) and duration. - Given a certificate of destruction is generated or downloaded, When the action occurs, Then events certificate.generated and certificate.downloaded are recorded with certificate_id and digest. - Given API access performs retention or audit operations, When an API call is authenticated, Then audit events api.requested and api.responded are recorded with route, method, status, and caller identity (service/user).
In-App Audit Log Search and Filters
- Given a user with Audit:Read permission, When they open the Audit view, Then they can filter by user/actor, client, artifact_type, event_type, and time range (absolute and relative) and results reflect all active filters. - Given a query returning ≤100,000 records, When the first page (50 rows) is requested, Then it loads within 2 seconds at the 95th percentile and supports cursor-based pagination. - Given a user without Audit:Read permission, When they access the Audit view or API, Then the system returns HTTP 403 and no data is leaked. - Given results are listed, When a row is displayed, Then columns include timestamp_utc, event_type, actor, client (if any), artifact_type, target_id, outcome, and hash_prefix; clicking a row reveals full payload and per-record hash verification succeeds. - Given filters are changed, When Export is triggered, Then the export respects the currently applied filters and sort order.
Evidence Export to PDF/CSV/JSON
- Given filtered audit results, When a user exports to CSV, Then the system generates an RFC 4180-compliant file with header row, includes all fields, and an accompanying SHA-256 checksum; the job supports up to 1,000,000 records via async processing. - Given filtered audit results, When a user exports to JSON, Then the system returns newline-delimited JSON (NDJSON) with one event per line and includes an export metadata block (export_id, generated_at_utc, filters, record_count) and SHA-256 checksum. - Given filtered audit results, When a user exports to PDF, Then the system produces a paginated, readable PDF including header metadata (filters, generated_at_utc, record_count) and a QR/link to verify the export digest. - Given an export is requested, When it completes, Then a time-bound download link (valid ≤24 hours) is issued; access requires Audit:Read and export_id; unauthorized requests return HTTP 403 and are audited. - Given any export is created, When logging occurs, Then an audit event evidence_export.created is recorded with export_id, format, record_count, and digest.
Streaming SIEM Integration (API/Webhook)
- Given a subscriber configures a webhook with a shared secret, When audit events occur, Then payloads are delivered within 5 seconds median latency, signed (HMAC-SHA256 of body), and include idempotency_key, sequence, and delivery timestamp. - Given a webhook delivery fails, When the endpoint returns non-2xx, Then retries occur with exponential backoff for up to 24 hours; duplicates may occur but retain the same idempotency_key; final failure emits audit event webhook.delivery_failed. - Given a client consumes the streaming API, When it supplies a valid cursor, Then events are returned in order without gaps; an invalid/expired cursor returns HTTP 400 with error code cursor_invalid. - Given access control is enforced, When a token lacking audit.read scope calls the stream or webhook management endpoints, Then the request returns HTTP 403 and is audited. - Given a subscriber revokes credentials, When events are produced, Then no further deliveries occur and an audit event streaming.disabled is recorded.
Audit Log Retention Policy and Privacy Controls
- Given an admin sets audit log retention, When saved, Then audit log retention is managed separately from artifact retention and adheres to region-specific minimums; shorter-than-minimum values are rejected with validation error. - Given an artifact is purged per its retention, When the audit log retention exceeds the artifact retention, Then related audit records remain accessible until their own retention expires. - Given PII redaction is enabled for audit logs, When events are written, Then sensitive fields (e.g., client name, email) are redacted or tokenized while preserving referential integrity; original PII is not stored in audit payloads. - Given an audit record is under legal hold, When an audit purge job runs, Then the held record is not deleted; when the hold is released and retention is exceeded, Then it becomes eligible on the next purge. - Given an audit log purge occurs, When it completes, Then an audit event audit_log.purged is recorded with counts and a certificate (optional) is generated when configured.

Consent Ledger

Capture e‑signed consents and policy acknowledgments with versioning and per‑session linkage. Auto‑request re‑consent when policies change and flag sessions missing required authorizations before you start. A clear timeline proves who agreed to what and when, cutting disputes and streamlining audits.

Requirements

Legally-Binding E-Signature Capture & Evidence
"As an independent practitioner, I want clients to e-sign required consents from any device so that I have legally defensible proof of authorization."
Description

Implement a mobile-friendly e-sign flow for consents and policy acknowledgments that captures typed or drawn signatures, timestamps, IP address, device fingerprint, and optional geolocation, then generates an immutable evidence PDF. Store signed artifacts with encryption at rest, strict role-based access, and a cryptographic hash to detect tampering. Support single or multi-signer scenarios (e.g., client plus guardian), pre-filled client/practice fields, and offline-friendly, one-time signing links. Seamlessly attach the signed consent to the client record and make it retrievable across scheduling, notes, and invoicing contexts.

Acceptance Criteria
Versioned Consent Templates & Policy Management
"As an account owner, I want to manage versioned consent templates and tie them to services so that policy updates are tracked and enforced consistently."
Description

Provide a template system for consents and policies with versioning, effective dates, titles, categories (e.g., privacy, cancellation), and locale support. Allow rich-text editing with merge fields (client name, service, practice details), change logs, and archiving of superseded versions while preserving existing signatures. Enable mapping of required templates to services/session types and define re-consent rules (e.g., major version requires re-consent). Ensure seamless rendering to printable PDFs and compatibility with the e-sign flow.

Acceptance Criteria
Create and Publish Versioned Consent Template
Given I am a Practice Admin When I create a new consent template with title, category, locale, rich‑text content, semantic version 1.0, and an effective date today or later Then the template is saved as Draft with the provided metadata and content And the combination of title+category+locale+version is validated as unique, rejecting duplicates with a clear error When I publish the draft Then the template status becomes Active with the specified effective date And the template appears in the template catalog and is available for service/session-type mapping And effective date must be >= the latest effective date of any prior version for the same template root
Merge Fields Validation and Preview
Given the editor displays the list of supported merge fields (e.g., client_name, service_name, practice_name) When I save a template containing any unknown merge token (e.g., {{foo}}) Then the save is blocked and the UI lists each unknown token by name and location When I open Preview and select a sample client and service Then all supported merge fields render with correct values and formatting in both preview and PDF And attempting to initiate e‑sign for a client lacking data for a required merge field blocks the flow with a clear, actionable error
Change Log and Audit Trail for Template Versions
Given a template exists When I edit metadata or content while in Draft and save Then a changelog entry records actor, timestamp, fields changed, and before/after values When I publish a version or create a new version from an existing one Then a changelog entry records version transition (e.g., 1.0 → 1.1 or 2.0), effective date, and publish actor And the changelog is immutable, viewable in the UI, and exportable to CSV And viewing a template version shows a diff of content changes including added/removed text and merge fields
Supersession, Archiving, and Effective Date Enforcement
Given version 1.0 of a template is Active with effective date T1 And version 2.0 is published with effective date T2 > T1 Then on T2, version 1.0 status becomes Archived (Superseded) and is view‑only And all existing signatures linked to version 1.0 remain accessible and immutable And attempts to edit or delete an Archived version are blocked with a 403/validation error When scheduling or starting sessions occurring on/after T2 for services mapped to this template Then clients without a signature for version 2.0 are flagged as "Consent Required" and the session start is blocked until signed When sessions occur before T2 Then a valid signature for version 1.0 satisfies the consent requirement
Service Mapping and Re‑Consent Rules
Given Template A is mapped as Required to Service S with re‑consent rules Major=Required and Minor=Optional And a client has a signed consent for Template A v1.0 When the client books a session for Service S Then no missing‑consent flag is shown When Template A v2.0 (major) is published Then all upcoming sessions for Service S for clients lacking v2.0 are flagged "Consent Required" And an automatic re‑consent request is sent to those clients When Template A v1.1 (minor) is published Then sessions are not flagged, but an optional re‑consent link is available if configured And removing the mapping stops new sessions from requiring Template A without invalidating existing signatures
Locale Selection and Fallback in E‑Sign Flow
Given a client has preferred locale fr‑FR and practice default is en‑US When initiating the e‑sign packet for required templates Then the system selects the fr‑FR version for each template if available And if a fr‑FR version is missing, the system falls back to the practice default locale consistently across all templates in the packet and logs the fallback And mixing multiple locales within a single signing packet is prevented with a clear message And if no suitable locale or fallback exists, the flow is blocked with an error instructing the admin to add the missing locale version
PDF Rendering Fidelity and E‑Sign Compatibility
Given a published template with headings, lists, tables, hyperlinks, and merge fields When generating a PDF preview Then the PDF reproduces the web preview layout (fonts, pagination, margins, headers/footers, page numbers) and resolved merge values And the PDF is produced within 5 seconds for a 10‑page template under normal load When the e‑sign flow is initiated for the template Then signature fields render at the correct locations and a completed signing returns a signed PDF linked to the exact template version used And the signed PDF is downloadable and printable without layout shifts
Session Authorization Gate & Linkage
"As a provider, I want SoloPilot to warn or block sessions missing required consents so that I don’t perform work without authorization."
Description

Before a session starts, automatically verify that the client has active signatures for all required templates associated with that session type. Surface status in calendar, intake, and the session start screen; warn or block start when consents are missing, with a quick-sign shortcut to collect on the spot. Upon completion, link the consent record to the session for traceability. Support admin override with mandatory reason capture and audit trail, plus API/webhook events signaling authorization status changes.

Acceptance Criteria
Resolve Required Consents for Session Type
Given a scheduled session with session type T configured with required consent templates A and B And client C has existing consent signatures across template versions with configured expiry and effective dates When the system evaluates authorization readiness using the session’s scheduled start time in the organization time zone Then the session is marked Authorized only if C has active signatures for the current effective version of A and B that are not expired, not revoked, and within configured validity windows And otherwise the session is marked Missing Consents with a list of missing template IDs and required versions
Surface Authorization Status Across Calendar, Intake, and Start Screen
Given the calendar view, booking/intake flow, and session start screen for session S When authorization readiness is evaluated Then each surface displays a consistent status indicator: Authorized or Missing Consents And the indicator exposes a tooltip or detail panel listing missing templates and a Quick Sign action And updates in real time after new signatures are captured without requiring a full page refresh
Org Policy: Block vs Warn on Missing Consents
Given organization policy is set to Block session start when required consents are missing And session S is Missing Consents When a provider attempts to start S Then access to the session workspace is blocked with a modal explaining which templates are missing and providing Quick Sign And no notes, timers, or billing actions can be initiated until authorization is satisfied or an admin override is applied Given organization policy is set to Warn (not Block) And session S is Missing Consents When a provider attempts to start S Then a warning is displayed listing missing templates with Quick Sign, and the provider may proceed without override
Quick Sign Collection at Session Start
Given session S is Missing Consents for templates A and B When the provider invokes Quick Sign from the session start screen Then the client is presented with the exact current effective versions of A and B for e‑signature And upon successful signatures, consent records are created with template ID, version, signer identity, timestamp, and IP/device metadata And the system re-evaluates authorization for S and updates status to Authorized in the same flow
Link Consents to Session on Completion
Given session S is started and later marked Complete And S was Authorized by specific consent record IDs (including any collected via Quick Sign) When S is completed Then the consent record IDs, template IDs, and versions are immutably linked to S And the linkage appears in the session timeline and Consent Ledger and is exportable via reports and API And linked records cannot be removed or altered without an auditable admin action
Admin Override With Mandatory Reason and Audit Trail
Given a user with Admin role attempts to override Missing Consents for session S When the admin selects Override to Start Then a reason text field is mandatory (minimum 10 characters) and the action is blocked until provided And the override entry captures admin user ID, timestamp, session ID, client ID, missing template IDs/versions, and reason And S is marked Authorized (Overridden) in UI while underlying client consent status remains unchanged And an audit log entry is created and visible to auditors
Authorization Status API and Webhook Events
Given a session S with an authorization status that may change due to signatures, overrides, policy or template updates When the status changes (e.g., Missing->Authorized, Authorized->Missing, Override created/removed) or when consents are linked on completion Then the system publishes webhook events with types authorization.updated, authorization.override.created, authorization.override.removed, and session.consent_linked And each payload includes org_id, session_id, client_id, session_type_id, status, missing_templates[], satisfied_templates[], effective_at timestamp, and actor context (system/user) And a GET endpoint returns the current authorization status for S with the same fields for API consumers
Auto Re-Consent Triggers & Reminders
"As a practice owner, I want updated policies to trigger re-consent requests automatically so that clients are always on the latest terms without manual follow-up."
Description

When a new template version is published with re-consent required, automatically identify impacted clients and queue re-consent requests via email/SMS/portal with customizable messaging, reminder cadence, and expiry windows. Support rolling enforcement by effective date, pause-or-restrict scheduling for overdue clients, and provide progress dashboards and filters. Localize requests, offer deep links for one-tap signing, and record all delivery/engagement events in the client ledger for auditability.

Acceptance Criteria
Auto-Identify Impacted Clients on Template Publish
Given a new consent template version (vX) is published with "re-consent required" and an effective date is set When the publish action is confirmed Then the system identifies all clients whose most recent signed consent version is older than vX and whose status is Active or Prospective And the system excludes clients whose status is Archived or who have a Do-Not-Contact flag for all available channels And the identification and queue preparation completes within 10 minutes of publish time And the system displays the total impacted client count and breakdown by channel eligibility (email, SMS, portal)
Queue Multi-Channel Re-Consent Requests
Given impacted clients have been identified And customizable message templates exist for email, SMS, and portal notification When the re-consent queue is generated Then each eligible client is assigned an initial send via all enabled channels for which valid contact info and consent exist And each send uses the selected message template variant and includes a deep link to the signing flow And initial sends are dispatched immediately or at the configured send window, whichever is later And each queued item has an associated expiry timestamp computed from the configured expiry window And queue entries store message template version, channel, locale, and intended send time
Reminder Cadence and Expiry Handling
Given a client has not completed re-consent after the initial request When reminder cadence is configured (frequency, max sends, quiet hours) Then reminders are scheduled per cadence and respect client time zone quiet hours And reminders stop immediately upon successful signing And the system enforces a maximum number of reminders per client per template version And on reaching expiry without signing, the request is marked Expired and no further reminders are sent And operators can pause/resume reminders per campaign or per client, with changes taking effect within 5 minutes
Rolling Enforcement by Effective Date
Given a re-consent-required template has an effective date set in the future When the current time is before the effective date Then clients may schedule sessions normally while continuing to receive re-consent requests When the current time is on or after the effective date Then clients without a valid signed latest version experience enforcement per configured mode: Pause (block scheduling) or Restrict (require signing before confirming) And the scheduling UI displays a blocking message with a one-tap deep link to sign And upon successful signing, scheduling is unblocked immediately and the original scheduling flow resumes without data loss
Localized Requests with Fallback
Given clients have a preferred language set and localized message/signing templates exist When re-consent requests are generated Then each request uses the client's preferred language for content and signing UI And if a localized template is unavailable, the system falls back to the workspace default language and records the fallback And all dates/times in messages are formatted in the client's locale and time zone And the chosen locale is recorded alongside each delivery and engagement event
Secure Deep Link One-Tap Signing
Given a re-consent request is sent via email or SMS When the client taps the deep link Then the client is taken directly to the signing screen for the correct template version without requiring login And the link is secured with a single-use, time-bound token tied to the client and template version And expired or previously used links show a safe error with an option to request a fresh link And upon signing, the client is redirected back to the originating context (e.g., scheduling) if applicable And link opens, invalidations, and completions are tracked as engagement events
Audit Trail and Progress Dashboard
Given re-consent deliveries and engagements occur across channels When events such as Queued, Sent, Delivered, Opened, Clicked, Bounced, Failed, Signed, and Expired happen Then each event is appended to the client’s consent ledger with timestamp, channel, locale, template version, actor/system, and outcome metadata in an immutable record And the progress dashboard shows real-time counts and trends by status, channel, client segment, and template version with filters for date range and effective date And dashboard metrics and lists update within 5 minutes of new events and support CSV export of visible results And clicking a metric segment drills down to the corresponding client list with current status
Client Consent Timeline & Ledger View
"As a provider, I want a clear consent history for each client so that I can answer disputes and audits quickly."
Description

Offer a consolidated, per-client timeline of consents and acknowledgments showing template name, version, signature parties, status (active, revoked, expired), linked sessions, and staff actions, with filters, search, and export. Enable capturing revocations with timestamp and reason, and surface visual diffs between template versions to clarify changes. Make the ledger accessible from client profiles and session views, ensuring quick answers during scheduling, note-taking, invoicing, and support.

Acceptance Criteria
Per-Client Consent Timeline Columns & Data Integrity
Given a client has at least Active, Expired, and Revoked consents across multiple template versions and signers When the user opens the client’s Consent Ledger Then each row shows Template Name, Version, Signer(s), Status (Active/Revoked/Expired), Linked Sessions (count), Last Staff Action, Created At, Updated At And values exactly match the underlying consent records And multi-signer consents display all signers’ full names and emails And status is computed as: Active if within validity and not revoked; Expired if end date < now and not revoked; Revoked if a revocation exists And clicking the Linked Sessions count reveals session IDs and dates and allows opening a session in a new tab
Filter, Sort, and Search Ledger
Given the ledger contains 1,000+ records When the user applies filters (Status, Template Name, Version, Date Range, Has Linked Session Yes/No) Then results update within 300 ms after data load And filters can be combined, individually cleared, and persisted in the URL for deep links And sorting by Date, Template, and Status works in both ascending and descending order and persists to export And the search matches Template Name, Signer Name, Signer Email, and Session ID, case-insensitive, supporting partial matches
Export Filtered Ledger
Given the user has applied filters and sorting When the user exports to CSV or PDF Then the export includes only the filtered results in the same sort order And includes columns: Template Name, Version, Status, Signer(s), Signed At, Revoked At, Revocation Reason, Linked Session IDs, Staff Action, Actor, Actor Role, Created At, Updated At, Client ID And CSV is UTF-8 RFC 4180 compliant with ISO 8601 timestamps including timezone And the file generates within 5 seconds for up to 10,000 rows And only users with Export Ledger permission can export; unauthorized attempts are blocked and logged And an export event records user, timestamp, filters, sort, and row count in the audit trail
Capture and Reflect Consent Revocation
Given a consent is currently Active When an authorized user revokes the consent Then the system requires a Revocation Reason (minimum 5 characters), captures Revoked At (UTC) and the acting user And the consent status updates to Revoked and appears accordingly in the ledger timeline and details And all future-dated linked sessions are flagged "Missing required consent" with a link to request re-consent And attempting to revoke an already Revoked consent shows a descriptive error and does not create a duplicate event And revocation details are included in exports and the audit trail
Visual Diff Between Template Versions
Given a consent references Template vN where vN-1 exists When the user selects "View changes from vN-1 to vN" Then a diff view highlights additions (green) and deletions (red) with section context preserved And the header shows Template Name, from/to versions, and effective dates And rendering completes within 2 seconds for templates up to 100,000 characters And if no previous version exists, the user sees "No previous version to compare" And the user can print or export the diff to PDF
Ledger Access From Client Profile and Session View
Given a user is on the Client Profile When they open the Consent Ledger tab Then the per-client ledger loads within 1 second and shows the correct client’s records Given a user is on a Session View When they open the Consent panel Then it lists linked consents (template and version) and any missing required consents with a "Request Consent" action And clicking a consent opens the ledger focused on that consent, with a back link returning to the originating session
Staff Actions Audit Trail in Ledger
Given staff perform actions (create/request/view/export/revoke) on consents When the ledger timeline is viewed with staff actions enabled Then each action entry shows Action Type, Actor Name, Actor Role, Timestamp, Affected Record, and IP/Device fingerprint (if available) And entries are immutable and ordered chronologically And users can filter by Action Type and Actor And audit entries are exportable subject to permission
Audit Export & Evidence Pack
"As a business owner, I want to export a complete evidence pack of consents so that I can satisfy audits and resolve disputes efficiently."
Description

Generate audit-ready exports by client, date range, or service that bundle signed documents, evidence summaries (timestamps, IP, device), template version metadata, and a manifest containing cryptographic hashes for integrity verification. Provide PDF/CSV outputs and secure, expiring share links with access logging. Ensure exports align with common compliance reviews to streamline audits and dispute resolution.

Acceptance Criteria
Filtered Export Creation
Given a user with Export_Audit permission selects any combination of Client, Date Range, and Service filters and clicks Generate Export When the export job is queued and processed Then the resulting export contains only consents and sessions that match all selected filters And the export status is visible with progress states Pending, Processing, Completed, or Failed And on success a single downloadable bundle is created and associated with a unique export_id
Evidence Summary Completeness
Given an export includes any consent record When the evidence summary CSV is generated Then each row includes client_id, client_name, session_id, consent_id, consent_title, template_version_id, signer_name, signer_email, signer_id, request_timestamp_utc, view_timestamp_utc, signature_timestamp_utc, signer_ip, signer_user_agent, signer_auth_method, document_sha256, export_id And all timestamps use ISO 8601 in UTC with Z suffix And the CSV is UTF-8 encoded with a single header row and consistent column order across exports
Manifest and Integrity Verification
Given an export bundle is generated When the manifest file is produced Then the manifest lists every file path in the bundle with its SHA-256 hash and byte size And the manifest includes hashing_algorithm, export_id, generator_version, and created_at_utc And recomputing SHA-256 for any file in the bundle matches the value in the manifest And altering any file causes a hash mismatch detectable by verification
Secure Expiring Share Link and Access Logging
Given a completed export exists When a user creates a share link with a chosen expiry between 24 and 168 hours Then the platform generates an unguessable tokenized URL scoped to that export and organization And access attempts and downloads are logged with timestamp_utc, ip, country_region, user_agent, and token_id And the link automatically becomes invalid after expiry or when revoked by the owner And access after expiry or revocation returns HTTP 410 and is logged
Export Packaging and Formats
Given an export completes successfully When the bundle is assembled Then it is packaged as a single ZIP archive And the archive contains a documents folder with all signed consent PDFs And the archive contains evidence.csv, manifest.json, and metadata.json with template version metadata And PDFs are flattened and render in standard PDF viewers without errors And CSV and JSON files are valid, UTF-8 encoded, and pass schema validation And filenames include stable identifiers and ISO 8601 timestamps
Compliance Review Readiness
Given an auditor reviews the export When they open the bundle Then README_AUDIT.txt summarizes scope filters, generator_version, created_at_utc, hashing_algorithm, and verification steps And a mapping file links sessions to consents and indicates any missing or expired required authorizations at the time of each session And the bundle includes template policy versions with effective_date and change_summary And no records outside the requested scope are present And client notes are excluded from the bundle
Export Failure Handling and Retry
Given an export job encounters a recoverable error When the system retries the job up to 3 times with exponential backoff Then the final status is Completed or Failed with an error_code and error_message recorded And the requesting user is notified on completion or permanent failure And partial or incomplete bundles are not available via share links

Access Guardrails

Share the minimum necessary with expiring, watermarked links, field‑level masking, and IP/time‑window restrictions. Break‑glass access requires a reason and notifies you, logging every view and export. Keeps sensitive details protected while still enabling compliant collaboration with clients, supervisors, or reviewers.

Requirements

Expiring, Watermarked Share Links
"As an independent consultant, I want to send an expiring, watermarked link to a session note so that a reviewer can view it briefly without being able to reshare or retain unauthorized copies."
Description

Enable users to generate secure share links from notes, invoices, session summaries, and attachments with configurable expiration (minutes to days), optional single-use access, and viewer verification (email OTP or passcode). Apply dynamic watermarks that overlay recipient identity, access timestamp, link ID, and workspace name on every page/view. Offer download/print controls with a server-rendered read-only viewer to minimize data exfiltration. Provide an immediate revocation kill switch and real-time access metrics (opens, last accessed). Integrate with SoloPilot automations (e.g., one-click share during session-to-invoice) and honor workspace defaults. Links use signed tokens, are rate limited, and include localization/timezone support for expiry and watermark timestamps.

Acceptance Criteria
Configurable Expiry & Timezone Support
Given a user creates a share link with expiry set to 45 minutes in the workspace timezone When 45 minutes have elapsed from the creation timestamp Then the link denies access with an “Expired” message and HTTP 410, and an audit event is recorded Given a user sets expiry to 3 days When 72 hours have elapsed regardless of the viewer’s timezone Then the link is invalid and cannot be accessed via API or UI Given no explicit expiry is selected When the link is created Then the workspace default expiry is applied and displayed to the sender and viewer in their localized format/timezone
Single‑Use Access Behavior
Given a link is configured as single‑use When the first viewer completes verification and the content loads Then the link is immediately marked consumed and any subsequent access attempts return HTTP 410 with a single‑use consumed message Given a link is configured as single‑use When a viewer fails verification or closes before the content is served Then the single‑use is not consumed and the link remains usable Given a single‑use link is forwarded to multiple people When two people attempt access Then only the first successful verified access is allowed and all others are rejected
Viewer Verification (Email OTP or Passcode) with Token Integrity & Rate Limiting
Given Email OTP is selected and a recipient email is provided When the viewer enters the correct OTP within the configured validity window Then access is granted and the verification success is logged with timestamp and recipient identifier Given Email OTP is selected When the viewer requests multiple OTPs Then only the latest OTP is valid and OTP requests are rate‑limited (e.g., max 5 per hour per link); exceeding limits returns HTTP 429 and is logged Given Passcode verification is selected When the viewer enters the correct passcode Then access is granted; incorrect attempts are rate‑limited (e.g., 10 attempts per hour per IP) and lockout is enforced on excess Given a share URL contains a signed token When the token or query parameters are tampered with Then access is denied with HTTP 401/403 and the attempt is recorded; no content is leaked
Dynamic Watermark Overlay on All Views & Prints
Given a viewer opens shared content (notes, invoices, session summaries, PDFs, images, attachments) When any page/screen is rendered Then a dynamic watermark overlays recipient identifier, access timestamp (localized), link ID, and workspace name on every page/view and cannot be hidden via client‑side CSS Given printing is allowed When the viewer prints the content Then the printed output contains the same watermark information on every page Given downloading is allowed When a downloadable artifact is generated Then the server produces a watermarked file; no unwatermarked originals are exposed through the share link
Read‑Only Server‑Rendered Viewer with Download/Print Controls
Given download is disabled for a share link When the viewer attempts to download via UI actions or direct URL guessing Then no endpoint returns the original asset; only server‑rendered content is streamed with no‑store caching and content‑disposition prevents file download Given print is disabled for a share link When the viewer invokes browser print Then the viewer displays a “Printing disabled” overlay and prevents rendering of underlying content Given both download and print are enabled When the viewer downloads or prints Then the outputs are served/printed through the server‑rendered pipeline and include watermarks
Immediate Revocation Kill Switch
Given a share link exists When the owner clicks Revoke Then the link becomes invalid within 5 seconds; new requests return HTTP 410 and open sessions display an “Access revoked” state on next request/poll Given a link was revoked When the viewer retries access Then metrics stop incrementing and no content is returned via API or UI
Real‑Time Access Metrics & Automation Defaults
Given a share link is accessed When the content is successfully rendered Then the opens count increments within 5 seconds and Last Accessed updates with a localized timestamp Given workspace defaults for expiry, verification, and controls are configured When a link is created via one‑click share during session‑to‑invoice automation Then the link is created with those defaults applied and an audit event records the automation source Given workspace defaults are later changed When new links are created Then new links honor the updated defaults while existing links remain unchanged
Field-level Masking & Minimum-Necessary Views
"As a therapist, I want to mask PHI fields and sensitive note sections before sharing with a supervisor so that they only see the minimum necessary information to perform their review."
Description

Provide configurable field-level masking across Client profiles, Session Notes (by section/field), Invoices (line items, rates), and custom fields to enforce minimum-necessary disclosure. Support mask types: hide, redact with label, and partial reveal (e.g., last 4). Masks apply consistently across the web viewer and any allowed exports (PDF/CSV), with server-side redaction to prevent transmission of hidden values. Allow per-share selection of a mask set or template override and a sharer preview to confirm what recipients will see. Preserve functional context (calculations, totals) while suppressing sensitive inputs. Record the mask set used for each share for auditability and integrate with automation flows and policy templates.

Acceptance Criteria
Cross-Entity Mask Application with Server-Side Redaction
Given a defined mask set that specifies masks for Client Profile fields, Session Notes fields/sections, Invoice fields (including line-item rate/description), and Custom Fields And a recipient role allowed to view a shared record When the sharer creates a link selecting that mask set Then the recipient’s web view renders all specified fields according to the mask definitions And the corresponding PDF and CSV exports contain only the masked values And the server responses (HTML/JSON/PDF/CSV) never transmit the original unmasked values for those fields (verified via network inspection) And attempting to access masked values via API using the share token returns masked values only
Mask Type Behaviors: Hide, Redact with Label, Partial Reveal
Given three mask types are available: Hide, Redact-with-Label, Partial-Reveal When Hide is applied to a field Then the field displays a neutral placeholder (e.g., “—”) and is omitted from exports When Redact-with-Label is applied with a configured label (e.g., “REDACTED: Diagnosis”) Then the UI and exports display exactly that label in place of the value When Partial-Reveal (Last 4) is applied to an alphanumeric field Then only the last 4 characters are visible and the remaining characters are consistently masked (e.g., •) And if the value length is fewer than 4 characters, the entire value is redacted per policy And mask rendering is identical in web view and exports
Per-Share Mask Set Selection and Sharer Preview
Given a default policy template with an associated mask set And a user initiates a share flow When the user selects a different mask set or applies per-field overrides for this share Then the selection applies only to that specific share and does not mutate the underlying template And a “Preview as Recipient” shows exactly what the recipient will see with masking applied And opening the share link in a clean session matches the preview pixel-for-pixel/content-for-content And the share cannot be completed until the user acknowledges the preview confirmation
Preserve Functional Context: Calculations and Totals
Given an invoice with multiple line items, taxes, discounts, and adjustments And masks hide line item rates and descriptions When the recipient views the invoice via web or export Then invoice totals, taxes, and balances remain visible and numerically correct to the cent And no masked inputs leak via subtotals, tooltips, column headers, or CSV columns And rounding rules and currency formatting remain unchanged And calculations are performed server-side on stored values, with only allowed outputs transmitted
Auditability: Record Mask Set and Overrides per Share
Given a share is created using a specific mask set and any per-field overrides When the share is saved Then an immutable audit record stores: share ID, mask set ID/name, template ID (if any), override summary, sharer user ID, timestamp, and target entity IDs And each view/export event logs timestamp, viewer identity (if known), IP, user agent, and the mask set in effect And the audit trail is viewable in the UI and retrievable via API with appropriate permissions And attempts to modify past audit entries are blocked and logged as violations
Automation and Policy Template Integration
Given an automation rule that triggers a share or export (e.g., after session completion) And the rule references a policy template with a defined mask set When the automation runs Then the generated share links and files apply the template’s mask set server-side And if the automation step specifies an override mask set, that override is applied to that run only And PDFs/CSVs generated by automation contain masked values only; unmasked values are never transmitted And updating the template later does not retroactively alter mask sets on already-generated shares/exports
IP and Time-Window Access Controls
"As a freelancer, I want link access limited to my client’s office IP range and business hours so that off-hours or remote attempts are blocked by default."
Description

Allow owners to define IP allow/deny lists (single IPs and CIDR ranges) and access time windows at the workspace, template, and per-link levels. Enforce business-hour schedules with timezone selection and handle daylight savings transitions. Provide optional geo-based rules and a mobile-network allowance toggle. When access is outside policy, show a friendly denial page with the option to request break-glass access. Persist a snapshot of the applied rules with the link for consistent evaluation and auditing. Integrate with expiring links and masking so all controls evaluate before content is served, and log the specific policy rule that triggered an allow/deny decision.

Acceptance Criteria
Workspace IP Allow/Deny Enforcement with CIDR
Given a workspace has an IP allow list containing 203.0.113.0/24 and 198.51.100.10 and an IP deny list containing 203.0.113.55 When a request originates from 203.0.113.42 Then access is allowed and the audit log records decision=allow and matched_rule=allow:workspace:cidr:203.0.113.0/24 Given the same configuration When a request originates from 198.51.100.10 Then access is allowed and the audit log records decision=allow and matched_rule=allow:workspace:single:198.51.100.10 Given the same configuration When a request originates from 203.0.113.55 Then access is denied and the audit log records decision=deny and matched_rule=deny:workspace:single:203.0.113.55 and precedence=deny_over_allow Given the workspace has any IP allow list configured When a request originates from 192.0.2.25 not present in the allow list Then access is denied and the audit log records decision=deny and matched_rule=implicit_deny_outside_allowlist
Template Business-Hours Access Window with Timezone and DST
Given a template access schedule of Mon–Fri 09:00–17:00 in timezone America/New_York When a request occurs at 13:00 UTC on July 15 Then access is allowed and the audit log records evaluated_timezone=America/New_York and local_time=09:00 and decision=allow Given the same schedule When a request occurs at 14:00 UTC on December 15 Then access is allowed and the audit log records evaluated_timezone=America/New_York and local_time=09:00 and decision=allow Given the same schedule When a request occurs at 21:01 UTC on July 15 Then access is denied and the audit log records evaluated_timezone=America/New_York and local_time=17:01 and decision=deny Given the same schedule When a request occurs on Saturday at 14:00 UTC Then access is denied and the audit log records decision=deny and reason=outside_business_hours
Rule Precedence Across Levels (Per-Link > Template > Workspace)
Given a workspace deny list contains 198.51.100.0/24 and a per-link allow list contains 198.51.100.10 and the template has no IP rules When a request to that link originates from 198.51.100.10 Then access is allowed and the audit log records decision=allow and matched_rule=allow:link:single:198.51.100.10 and evaluation_path=[link,template,workspace] Given a workspace allow list contains 203.0.113.0/24 and a template deny list contains 203.0.113.42 and the link has no IP rules When a request to a document using that template originates from 203.0.113.42 Then access is denied and the audit log records decision=deny and matched_rule=deny:template:single:203.0.113.42 Given no link or template rules and a workspace allow list contains 203.0.113.0/24 When a request originates from 192.0.2.10 Then access is denied and the audit log records decision=deny and matched_rule=implicit_deny_outside_allowlist
Per-Link Policy Snapshot Persisted and Enforced
Given a link is created and the effective policy is computed from workspace, template, and per-link rules When the link is saved Then a read-only snapshot of the effective policy is persisted with the link including version and hash identifiers Given the persisted snapshot for the link When workspace or template rules are later changed Then access decisions for that link continue to evaluate against the persisted snapshot and the audit log records policy_snapshot_applied=true and snapshot_id and snapshot_hash Given a request to the link When an access decision is made Then the audit log includes the snapshot_id used for evaluation and the matched_rule that produced the decision
Pre-Serve Evaluation with Expiring Links, Geo Rules, and Masking
Given a link that expires at 2025-12-31T23:59:59Z and geo allow list includes US and IP allow list includes 203.0.113.0/24 and masking rules are configured When a request from IP 203.0.113.42 with geo=US occurs on 2025-06-01T12:00:00Z Then all controls are evaluated before any content is sent and access is allowed and masked fields are applied in the response and the audit log records decision=allow and matched_rule and all_controls_evaluated=true Given the same configuration When a request occurs on 2026-01-01T00:00:00Z Then access is denied and the denial page is shown and the audit log records decision=deny and matched_rule=deny:link:expired Given the same configuration When a request from IP 203.0.113.42 with geo=FR occurs before expiry Then access is denied due to geo rules and the audit log records decision=deny and matched_rule=deny:*:geo:country:FR
Mobile Network Allowance Toggle Enforcement
Given a per-link mobile-network allowance toggle is enabled and the IP allow list does not include 198.51.100.20 and the request network_type is detected as mobile When a request originates from 198.51.100.20 Then access is allowed and the audit log records decision=allow and matched_rule=allow:link:mobile_toggle and network_type=mobile Given the same allow lists and network conditions but the mobile-network allowance toggle is disabled When a request originates from 198.51.100.20 Then access is denied and the audit log records decision=deny and matched_rule=implicit_deny_outside_allowlist and network_type=mobile Given the mobile-network allowance toggle is enabled When a request originates from a desktop broadband network outside the allow list Then access is denied and the audit log records decision=deny and network_type=fixed
Denial Page with Break-Glass Request, Notification, and Logging
Given access is denied due to any policy rule When the denial page is rendered Then the page displays a human-friendly explanation and the matched rule code and a button to Request Temporary Access (break-glass) Given the user clicks Request Temporary Access and provides a non-empty reason When the request is submitted Then temporary access is granted for the configured break-glass duration and the owner is notified via email and in-app notification with requester, reason, link, IP, and geo details and the audit log records decision=allow and mode=break_glass and reason_text Given break-glass mode is active for the link When the user views or exports content Then each view and export is logged with mode=break_glass and event_type in {view, export} Given the break-glass duration has expired When the user attempts access again Then access is denied and the denial page is shown and the audit log records decision=deny and matched_rule=deny:link:break_glass_expired
Break-Glass Access Workflow with Justification and Alerts
"As a supervisor, I want to request temporary access with a justification when a link is blocked so that I can review urgent cases while preserving accountability and an audit trail."
Description

Introduce a just-in-time access flow when a recipient is blocked by policy or needs elevated scope. Require a reason for access, intended duration within policy limits, and acknowledgment of terms. Support optional approver routing (owner or delegated approvers) or auto-approval for predefined emergency rules. On approval, grant temporary, scope-limited access (extended time window, relaxed IP, or expanded mask set) and auto-expire it after the approved duration. Notify the owner in real time (email, push, in-app) and provide a one-click revoke. Enforce cooldowns and rate limits on repeated break-glass attempts. Capture and store the justification, approver, timestamps, and effective policy changes for complete traceability.

Acceptance Criteria
Blocked Recipient Requests Break-Glass with Required Fields
Given a recipient is blocked by policy when attempting to access a protected resource When they initiate a break-glass request Then the system requires a reason (minimum 10 characters), an intended duration, and acknowledgment of terms before submission And Submit remains disabled until all required fields are valid And The requested duration cannot exceed the configured maximum; violations display a validation error and prevent submission And The system logs requester ID, resource ID, IP, reason, requested duration, and timestamp upon submission
Owner/Delegate Approval and Emergency Auto-Approval
Given break-glass is configured with approver routing When a request is submitted Then the owner and all delegated approvers receive an approval task with requester, resource, reason, requested duration, and scope deltas And Approvers can Approve or Deny; the decision is recorded with approver identity and timestamp And If a predefined emergency rule matches, the system auto-approves, records the rule ID, and bypasses human approval And If no decision is made within the configured SLA, the request escalates per policy and notifies the requester And Denied requests keep access blocked and notify the requester; an immutable audit entry is created
Scoped Temporary Access Granted and Auto-Expired
Given a break-glass request is approved (manual or auto) When temporary access is granted Then only the approved scope deltas are applied (e.g., extended time window, relaxed IP, expanded mask set) within policy limits And Access starts immediately and ends exactly at the approved expiry time And Upon expiry, active sessions are terminated and baseline policy is restored And The owner can invoke one-click revoke at any time; revocation immediately terminates sessions and restores baseline policy And All grant, revoke, and expiry events are logged with timestamps
Real-Time Owner Notifications with Revoke Link
Given a break-glass request is submitted or approved When notifications are dispatched Then the owner receives real-time notifications via email, push, and in-app (at least one channel must succeed) And Notifications include requester, resource, reason, requested duration, approved scope, and an approve/deny or revoke link as applicable And Delivery outcomes per channel are recorded; failures are retried according to channel policy And The revoke link performs immediate revocation when invoked and confirms success to the owner
Cooldowns and Rate Limits on Repeated Attempts
Given a user submits repeated break-glass requests against the same resource or policy set When requests exceed the configured rate limit or occur within the cooldown window Then subsequent requests are blocked with a clear error explaining remaining time or limit And The event is logged with requester, resource, and counter/cooldown metadata And Counters and cooldowns reset per policy after a decision (approve/deny) or after the cooldown elapses
Comprehensive Audit Trail and Export
Given any break-glass lifecycle events occur (request, decision, grant, revoke, expiry) When an auditor views or exports logs Then the system presents justification text, approver(s), all timestamps, requester IP, resource ID, effective policy changes (scope deltas), notification events, and all views/exports during the access window And Audit records are immutable, time-ordered, and filterable by requester, resource, date, and outcome And Audit data is exportable in CSV and JSON with complete field coverage
Comprehensive Audit Logging & Tamper-Evident Exports
"As an account owner, I want a complete, verifiable access log of views and exports so that I can demonstrate due diligence and investigate any suspicious activity."
Description

Record immutable audit events for every view, export, download attempt, policy change, revocation, and break-glass action. Each event includes actor identity (verified email or guest claim), timestamps, IP, user agent, link ID, object type/ID, applied template, mask set, decision (allow/deny), and reason. Provide per-object timelines and a workspace-level log with filtering, search, and retention settings. Enable exports (CSV/JSON) and webhook delivery to external monitoring tools, with signed digests or hash chains to verify log integrity. Support encrypted at-rest storage of logs and pseudonymization where appropriate to limit exposure. Surface incident-ready evidence views summarizing who accessed what, when, from where, and under which policy.

Acceptance Criteria
Immutable Audit Event Recording for All Access and Policy Actions
Given a user or guest performs a view, export, download attempt, policy change, revocation, or break-glass action When the action is executed Then exactly one immutable audit event is appended to the log And the event includes actor identity (verified email or guest claim), timestamp (UTC ISO 8601), IP, user agent, link ID (if applicable), object type/ID, applied template, mask set, decision (allow/deny), and reason (if provided or required) And the event has a unique event ID and write-once persistence And no UI or API can edit or delete the event after creation
Tamper-Evident Hash Chains and Integrity Verification
Given audit events are appended over time When integrity verification is run for a selected time window or sequence Then the hash chain or signed digest validates successfully if no events were altered, inserted, or removed And any modification, reordering, or deletion causes verification to Fail and identifies the first compromised segment And retention purges preserve chain integrity by anchoring/verifying digests before deletion And verification results are accessible via UI and API
Per-Object Timelines and Workspace Log with Filter, Search, and Retention
Given a user opens an object's audit timeline or the workspace-level log When they apply filters (actor, action type, decision, date range, IP, link ID) or search (actor email, object ID, reason text) Then only matching events are displayed in strict chronological order with precise timestamps And the same filters/search are supported in both object timelines and the workspace log And retention can be configured per workspace and is enforced automatically And retention configuration changes are logged as policy change events And purged events are not retrievable via UI, API, export, or search
Log Export in CSV/JSON with Verifiable Integrity
Given a user exports audit events as CSV or JSON for a selected filter set When the export completes Then the export contains exactly the matching events in deterministic order And a companion integrity artifact (signed digest or hash chain manifest) is produced And a verification API/CLI validates the export and returns Pass for untampered files and Fail if any byte is altered And exported fields match the defined event schema without omission or addition
Webhook Delivery to External Monitoring with Signed Payloads
Given a workspace has a configured and enabled webhook endpoint When new audit events are created Then events are delivered to the endpoint in near real time (<= 60 seconds) with signed payloads that allow integrity verification And non-2xx responses trigger retries with exponential backoff up to the configured limit And deliveries are idempotent using a stable event ID/signature to prevent duplicates And delivery failures, disables, and configuration changes are recorded as audit events
Encrypted At-Rest Logging and Pseudonymization Controls
Given audit logs are stored at rest When storage and access are evaluated Then all log data is encrypted at rest using a managed KMS with periodic key rotation per policy And only authorized roles can decrypt logs for display or export And pseudonymization is applied to sensitive fields (e.g., IP, email) in non-admin views/exports, with reversible reveal requiring elevated permission and captured reason And pseudonymization settings and reveals are recorded as audit events
Incident-Ready Evidence Views and Exports
Given an admin initiates an incident review for a defined time range and scope (object, actor, or link) When the evidence view is generated Then it summarizes who accessed what, when, from where, and under which policy, including first/last access and counts And it links to the applicable policy versions and the underlying raw audit events And it can be exported as a tamper-evident PDF/CSV bundle with an integrity signature And generating or exporting the evidence is recorded as an audit event
Share Policy Templates & Workspace Defaults
"As a solo practitioner, I want reusable share policy templates I can apply with one click so that sharing stays consistent and compliant without extra setup each time."
Description

Offer prebuilt and customizable policy templates (e.g., Client View, Supervisor Review, External Auditor) that bundle expiry, watermarking, mask sets, export permissions, IP/time rules, and break-glass behavior. Allow admins to set workspace-level defaults by object type and automation (e.g., session-to-invoice shares use Client View for 7 days). Enable one-click template application during sharing, bulk-update of active links when a template changes, and versioning to track policy evolution. Provide an API for managing templates, a recipient preview to validate outcomes, and analytics to report template usage, overriden shares, and risky exceptions.

Acceptance Criteria
Prebuilt and Custom Policy Templates Available and Editable
Given I am a workspace admin viewing Policy Templates, When I open the template catalog, Then I see system-prebuilt templates named "Client View", "Supervisor Review", and "External Auditor". Given I clone a prebuilt template, When I save it with a unique name, Then the cloned template is active and listed in the catalog. Given I create or edit a template, When I set an expiry between 1 and 90 days inclusive, Then the value is accepted; When outside that range, Then a validation error blocks save. Given I configure watermarking with tokens {recipient_email}, {timestamp}, and {workspace_name}, When I save, Then the template summary shows watermarking enabled and tokens listed. Given I select a field-level mask set, When I save, Then masked fields are indicated in the template summary and persisted. Given I set export permissions (None, PDF, CSV, All), When I save, Then the selection persists and is enforced on shares. Given I add IP rules in CIDR format and a UTC time window (start/end), When values are valid, Then save succeeds; When invalid, Then save is blocked with a clear error. Given I enable break-glass, When a recipient invokes break-glass, Then a reason (min 10 characters) is required, the owner is notified, and the event is audit-logged.
Workspace Defaults by Object Type and Automation
Given I am an admin, When I set workspace defaults mapping object types (Session, Note, Invoice) to specific policy templates, Then new manual shares for those object types preselect the mapped template. Given I configure the "session-to-invoice" automation default to use Client View with 7-day expiry, When the automation generates a share, Then the created link uses the Client View template and a 7-day expiry. Given I change a workspace default, When new shares are created thereafter, Then they use the updated default; existing shares remain unchanged until a bulk update is executed. Given I clear a workspace default for an object type, When a user initiates a share for that object type, Then the template must be explicitly selected before save.
One-Click Template Application in Share Flow
Given the share dialog is open for an object, When I click a template chip (e.g., Client View), Then all policy fields populate from the template within 500 ms and a summary panel reflects the applied rules. Given my role lacks override permission, When a template is applied, Then policy fields are read-only and the share can be saved without further edits. Given my role has override permission, When I change a policy field after applying a template, Then the UI labels the share as "Overridden" and requires an override reason before saving. Given I save the share, Then the resulting link's policy exactly matches the applied template values plus any permitted overrides, and the template name/version are recorded on the link.
Bulk Update Active Links on Template Change
Given a template is edited and saved as a new version, When I choose Bulk Update from the template actions, Then a dry-run displays the count of active links bound to that template and a summary of impacted policies. Given I start the bulk update with include_overrides=false, When the job completes, Then only non-overridden active links are updated to the new template version; overridden links are reported as skipped. Given the bulk update is running, Then I can view progress and, upon completion, a report showing counts of updated, skipped (expired or excluded), and failed links with error reasons. Given recipient notifications are enabled on the template, When policies become more restrictive (e.g., shorter expiry), Then affected recipients are notified within 10 minutes of update completion.
Template Versioning, History, and Rollback
Given I modify a template, When I save, Then the template version increments and stores editor, timestamp, and change notes in history. Given I open template history, When I select two versions, Then a diff view highlights changed fields (expiry, watermarking, mask sets, export permissions, IP/time rules, break-glass). Given I choose Roll Back to a prior version, When I confirm, Then that version becomes current and I am prompted to optionally trigger a bulk update to propagate it to active links. Given I view a share link created from a template, When I open its details, Then I see the applied template name and version identifier.
Template Management API Endpoints and Validation
Given I have an OAuth2 access token with templates:write scope, When I POST /templates with required fields, Then I receive 201 Created with template_id and current version. Given I GET /templates?status=active, Then I receive 200 OK with a paginated list of prebuilt and custom templates including version metadata. Given I PATCH /templates/{id} with If-Unmodified-Since-Version, When the version matches, Then I receive 200 OK with the incremented version; When it does not, Then I receive 412 Precondition Failed. Given I POST /templates/{id}/bulk-update with include_overrides boolean, Then I receive 202 Accepted with job_id, and GET /jobs/{job_id} returns progress and final counts (updated, skipped, failed). Given I POST /templates/{id}/preview with recipient_role and context, Then I receive 200 OK with resolved policy, redaction map, watermark sample, and access outcome for provided IP/time. Given I DELETE /templates/{id}, Then the template is soft-deleted (status=archived) and cannot be applied to new shares; existing links retain their applied version. All API calls are audit-logged with actor, timestamp, and request_id.
Recipient Preview and Analytics Reporting
Given the share dialog, When I open Recipient Preview and select a role (Client, Supervisor, External Auditor) and test IP/time, Then the preview renders masked fields, watermark overlay, and access allowed/denied consistent with the template rules, and shows the exact expiration timestamp. Given analytics is opened, When I filter by last 30 days, Then I see per-template usage counts, percentage of overridden shares, and counts of risky exceptions (unlimited expiry, exports allowed with no masks, empty IP rules) with drill-down lists of affected links. Given I click Export CSV in analytics, When the dataset is <= 100k rows, Then the file downloads within 60 seconds and matches the on-screen filters. Given a break-glass event occurs, When I view analytics within 5 minutes, Then the event is present with reason text, actor, timestamp, and affected object, and counts roll up to the template and owner.

Discovery Export

Produce court‑ready, immutable exports with Bates numbering, checksums, and a signed manifest. Built‑in redaction removes PII/PHI from selected fields or pages without altering originals, and a privilege log documents what was withheld. Delivers a zipped, searchable set that satisfies eDiscovery requests without manual tedium.

Requirements

Immutable Export Package with Signed Manifest
"As a practitioner responding to discovery, I want an immutable, signed export package so that recipients can verify integrity and admissibility of the produced materials."
Description

Produce a single, court-ready ZIP package that is immutable and verifiable. On export, generate a manifest that lists every file with its SHA-256 checksum, file size, MIME type, and original path, plus a package-level checksum. Digitally sign the manifest with SoloPilot’s export signing key and include timestamping to establish provenance. The package is set read-only and includes a human-readable verification guide and a command-line verification script. Originals in SoloPilot remain untouched; the export uses derivative, stamped copies where applicable. Optionally support password-protected encryption and separate out-of-band password delivery. Integrates with SoloPilot entities (notes, attachments, uploaded documents, invoices) and preserves key metadata in a companion JSON/CSV manifest for ingestion by eDiscovery tools.

Acceptance Criteria
Single ZIP Structure and Read-Only Packaging
Given an authorized user selects entities (notes, attachments, uploaded documents, invoices) and initiates Immutable Export When the export completes Then exactly one .zip file is produced at the chosen location And the archive contains the following top-level entries: /data/ (payload files), /manifests/manifest.json, /manifests/manifest.sig, /verify/verify.sh, /verify/verify.ps1, /docs/verification_guide.html And the .zip file is created with the read-only attribute set on supported filesystems (e.g., NTFS, APFS, ext4) And the archive contains no hidden/system artifacts such as .DS_Store, Thumbs.db, or __MACOSX And extracting the archive reproduces the same structure without additional files
Manifest Completeness and Package Checksum Accuracy
Given the produced export .zip When /manifests/manifest.json is parsed Then every file present under /data, /docs, and /verify is listed exactly once with fields: path, sha256 (64 lowercase hex chars), size_bytes (integer), mime_type, original_path (SoloPilot source path or URI) And the number of manifest entries equals the number of files present in those directories And recomputing SHA-256 and size for each listed file matches the manifest values And manifest entries are sorted by path ascending for deterministic ordering And manifest.json includes package_sha256 whose value equals the SHA-256 of the finalized .zip bytes
Manifest Digital Signature and Trusted Timestamp
Given the files /manifests/manifest.json and /manifests/manifest.sig in the export When the verification process validates the signature using the embedded SoloPilot export verification public certificate Then the signature on manifest.json verifies successfully And the signature includes an RFC 3161-compliant timestamp token And the timestamp is within ±5 minutes of the export_completed_at recorded in manifest.json And the certificate chain used to sign validates to the trusted SoloPilot export signing authority And any modification to manifest.json or manifest.sig causes signature verification to fail
Verification Guide and CLI Tamper Detection
Given an unmodified export on macOS, Linux, or Windows When the user follows /docs/verification_guide.html and runs /verify/verify.sh (POSIX) or /verify/verify.ps1 (PowerShell) Then the script completes with exit code 0 and reports that package_sha256 matches and all per-file hashes match manifest.json Given any exported file under /data is altered after export When the verification script is re-run Then it exits with a non-zero code and reports the specific mismatched path(s) And the verification guide provides OS-specific steps and requires only tools included in the package or native to the OS
Derivative Copies and Original Preservation
Given selected entities include items that require stamping or redaction When the export completes Then files in /data are derivative copies and not the system originals And the source entities’ last_modified_at and content checksums in SoloPilot remain unchanged by the export And each derivative file’s manifest entry includes source_uri and source_version linking back to the origin And the audit log for the export window shows no write operations to source entities
Optional Password-Protected Encryption
Given the user enables Encrypt package and supplies a password When the export completes Then the output is an encrypted container that cannot be opened or listed without the password And the password is not stored in the package, manifest, or any user-visible export artifact And after opening with the correct password, the verification scripts succeed for package_sha256, per-file hashes, and manifest signature And attempts to open or verify without the password fail with clear errors And the UI presents an out-of-band password delivery reminder at export time
Companion JSON/CSV for eDiscovery Ingestion
Given the export includes metadata.json and metadata.csv When these files are inspected Then both are UTF-8 encoded, with metadata.csv conforming to RFC 4180 and containing a header row And each file present under /data has exactly one corresponding metadata record And each record contains fields: entity_id, entity_type, source_uri, file_name, path, size_bytes, mime_type, created_at (RFC 3339), modified_at (RFC 3339), sha256 And metadata.json validates against metadata_schema.json included in /manifests And values shared with manifest.json (e.g., sha256, path, size_bytes, mime_type) match exactly
Bates Numbering and Stamp Rendering
"As a legal responder, I want configurable Bates numbering so that documents can be referenced consistently and unambiguously in legal proceedings."
Description

Apply configurable Bates numbering across all produced documents and pages, supporting continuous or per-document sequences, custom prefixes/suffixes, zero-padding, and restart rules. Render stamps non-destructively on export derivatives (e.g., PDF overlays), preserving originals. Allow placement (corners/center), font, color, and rotation options, with automatic collision avoidance for existing content. Generate a cross-reference file mapping Bates IDs to original document IDs, page numbers, and export filenames for easy lookup. Ensure numbering is deterministic given the same configuration and selection, and validate for gaps/duplicates before finalizing the package.

Acceptance Criteria
Continuous Bates Numbering Across Multi-Document Export
Given export configuration: numbering_mode=continuous, start_index=1, prefix="SP-", suffix="-A", zero_padding=7 And a selection resulting in a total of 27 pages in the export order When the export is executed Then each page receives a unique Bates ID formatted as "SP-" + 7-digit zero-padded index + "-A" And the sequence increments by 1 across all pages without gaps or duplicates And the first page is assigned "SP-0000001-A" and the last page is assigned "SP-0000027-A"
Per-Document Sequence with Configurable Restart Rules
Given export configuration: numbering_mode=per_document, restart_rule=each_document, start_index=1, zero_padding=6, prefix="CASE-", suffix="" And Document A has 3 pages and Document B has 2 pages When the export is executed Then Document A pages are labeled "CASE-000001" through "CASE-000003" And Document B pages restart at "CASE-000001" through "CASE-000002" And Bates IDs are globally unique across the package; if a duplicate would occur across documents, the export preflight fails with a duplicate-ID error
Non-Destructive PDF Overlay Stamp Rendering
Given export output is configured to generate PDF derivatives with stamp overlays And originals are stored in the repository When the export is executed Then a separate derivative file is created for each produced document And original files remain unmodified as verified by identical SHA-256 hashes before and after export And the Bates stamp is rendered as an overlay on the derivative (not altering the original content) on every stamped page
Placement Controls with Automatic Collision Avoidance
Given stamp placement=top-right, margin=12pt, font=Helvetica, font_size=10pt, color=#000000, rotation=0deg And a page contains existing content overlapping the intended stamp bounding box When the export is executed Then the engine adjusts the stamp position to avoid overlap by shifting inward up to 24pt; if still colliding, it attempts alternate corners in order: top-right, top-left, bottom-right, bottom-left, then center And the final placement maintains at least 6pt clearance from existing content and stays within page bounds And if no collision-free placement is possible, the page is flagged and the package fails prefinalization validation with a placement-collision error
Deterministic Reproducibility Given Identical Input
Given the same document selection, export order, and identical numbering and stamp configuration When the export is run twice Then the assigned Bates IDs are identical across runs for the corresponding pages And stamp placement coordinates are identical within ±0.1pt for each page And the cross-reference file content is byte-for-byte identical across runs And derivative filenames for each page are identical across runs
Cross-Reference File Generation and Integrity Checks
Given a completed export When the package is assembled Then a cross_reference.csv is included at the package root with headers: bates_id,original_document_id,original_page_number,export_filename And it contains exactly one row per stamped page And every bates_id value is unique and maps to an existing export file And original_document_id and original_page_number values resolve to the correct source document and page
Pre-Finalization Validation for Gaps and Duplicates
Given a package is pending finalization When the user initiates Finalize Then the system validates for numbering gaps, duplicates, and continuity according to the selected numbering mode And if any gap or duplicate is detected, finalization is blocked and a report lists the offending Bates IDs with document and page references And when all issues are resolved and validation passes, finalization is enabled
Redaction Engine for PII/PHI (Field- and Page-Level)
"As a practitioner, I want built-in redaction that removes PII/PHI without altering originals so that I can comply with privacy laws while producing responsive documents."
Description

Provide built-in redaction that removes PII/PHI and other sensitive content without altering originals. Support field-level redaction (selected SoloPilot fields such as client contact info) and page-level region redactions for PDFs/images with burn-in on exported derivatives only. Include pattern libraries (SSN, DOB, phone, email, addresses), custom regex, keyword lists, and named-entity detection for common identifiers. Offer a redaction review panel, preview before export, and a redaction summary report detailing what was redacted and why. Ensure redactions propagate to text layers and OCR output, and that visual artifacts are fully removed (no recoverable text under overlays).

Acceptance Criteria
Field-Level Redaction Preserves Originals
Given a client record containing PII/PHI fields (e.g., email, phone, postal address), and the user selects those fields for redaction When the user exports the discovery package Then the exported derivatives contain those fields replaced with [REDACTED] (or equivalent placeholder) across all rendered outputs and text layers And the original SoloPilot records and source files remain unchanged and fully readable within the app And the export indicates which fields were redacted by type and count And attempting to copy, paste, or text-extract the redacted field values from the derivatives yields no original content
Page-Level Region Redaction with Burn-In
Given a PDF/image document where the user has drawn redaction regions on specific pages and previewed the result When the user confirms the export Then the exported derivatives have those regions permanently burned-in (rasterized/vector-filled) so underlying content cannot be recovered And the corresponding text layer content within those regions is removed or replaced with placeholders And OCR of the derivatives does not output the original redacted text And zooming up to 1600% reveals no legible artifacts of the removed content And selecting text within the redacted regions is not possible
Pattern Libraries, Custom Regex/Keywords, and NER Detection
Given system pattern libraries for SSN, DOB, phone, email, and postal addresses are enabled, plus named-entity detection for common identifiers When automated detection is run on selected documents Then candidates are highlighted with type labels and confidence scores And users can enable/disable individual detectors and adjust a confidence threshold And users can add custom regex patterns and keyword lists that immediately participate in detection And disabling a detector removes its candidates from the review queue And on the provided test corpus, precision and recall for built-in patterns each meet or exceed 95% at default thresholds
Redaction Review Panel and Approval Workflow
Given detection results and manual markups exist for a set of documents When the user opens the redaction review panel Then the user can filter candidates by document, page, type, detector, and confidence range And approve or reject items individually or in bulk with keyboard shortcuts And add an optional reason/note per item or batch And only approved items are applied during export; rejected items are not And an audit log records user, timestamp, action (approve/reject/edit), and reason for each item
Redaction Summary Report Generation
Given an export containing applied redactions When the user generates the redaction summary report Then the report lists for each redaction: document ID/name, page number, coordinates (for regions) or field path (for field-level), redaction type (pattern/regex/keyword/NER/manual), approver, timestamp, and applied rule And provides totals by type and by document And excludes actual sensitive values from the report content And is included in the export as JSON and PDF artifacts And the report references the exact export job ID and time
No Recoverable Text or Artifacts Post-Export
Given a completed export with applied redactions When extracting content using standard tools (copy/paste, PDF text extraction, OCR engines) Then no original redacted content is present in the derivatives And redacted regions contain only non-text vector/raster content with no hidden or overlayed selectable text And full-text search for any redacted value returns zero hits And the exported OCR text substitutes placeholders (e.g., [REDACTED]) where applicable
Scoped Application, Whitelisting, and Fail-Safe Behavior
Given per-export configuration that defines which fields, detectors, and keywords are in scope and which terms are whitelisted When the export is run Then only in-scope items are redacted, and whitelisted terms remain visible And if any redaction operation fails for a file, that file is excluded from the deliverable and flagged with an actionable error; no partially redacted version is emitted And the UI surfaces the error with the affected file, step, and suggested remediation for retry
Privilege Log Auto-Generation
"As a practitioner, I want an automatically generated privilege log so that I can clearly document what was withheld or redacted and the reasons for opposing counsel."
Description

Automatically produce a privilege log documenting withheld items and redacted portions. Capture metadata required by standard discovery protocols: document ID, Bates range (if partially produced), document type, date, author/recipient, page count, and privilege basis (e.g., attorney–client, work product) with configurable reason codes. Output in CSV and JSON, attach to the export package, and link each entry to source objects in SoloPilot. Include validation to ensure each withheld/redacted item has a mapped reason, and present a preview with counts prior to export finalization.

Acceptance Criteria
Generate privilege log with required metadata
Given a Discovery Export includes withheld documents and documents with redactions And privilege reason codes are configured When the user starts the export build Then the system auto-generates a privilege log with one entry per withheld document and one entry per redacted portion And each entry includes: document_id, document_type, document_date (ISO 8601), author, recipient, page_count, privilege_basis, reason_code, and bates_range (if partially produced; otherwise "N/A") And all values reflect the state of the source at export time And the log is generated within 30 seconds for up to 10,000 entries
Validation blocks export when reasons are missing
Given at least one withheld or redacted item lacks a mapped reason_code or privilege_basis When the user attempts to finalize the export Then finalization is blocked and a validation banner lists the number of offending items And the preview table highlights the items needing mapping And the "Finalize Export" action remains disabled until all items have valid reason_code and privilege_basis And upon correction, validation re-runs automatically and passes without page reload
Attach CSV and JSON privilege log to export package
Given the export is finalized successfully When the export package is assembled Then privilege_log.csv and privilege_log.json are attached at the root of the ZIP and recorded in the export manifest And CSV uses UTF-8 with header: document_id,document_type,document_date,author,recipient,page_count,privilege_basis,reason_code,bates_range,source_object_id,source_link And JSON conforms to the published schema with the same fields and an array of entries And both files are downloadable from the Export detail view and have non-zero size
Preview privilege log with summary counts before finalization
Given the user is on the export preview step When privilege log data is computed Then a preview grid displays the first 100 entries with pagination And a summary shows counts of withheld documents, redactions, and totals by reason_code And counts equal the number of underlying items with exact match And the user can filter the preview by reason_code and item_type and the counts update accordingly
Deep links from privilege log entries to SoloPilot objects
Given a privilege log entry is shown When the user clicks its source_link Then SoloPilot opens the corresponding source object in a new tab And the source_object_id matches the object's immutable ID And access control is enforced: authorized users see the object; unauthorized users receive Access Denied And links remain valid for at least 1 year post-export
Bates range population for partial productions
Given a document is partially produced When the privilege log is generated Then the entry contains a bates_range in the format PREFIX000001–PREFIX000010 inclusive And the range exactly matches the produced pages’ assigned Bates numbers And fully withheld documents set bates_range to "N/A" And multi-range productions are represented as comma-separated ranges without overlap
Configurable privilege reason codes are enforced
Given admin-configured reason codes exist and are mapped to privilege bases When users assign reasons to items Then only active reason codes are selectable And the privilege log outputs both reason_code and privilege_basis as configured at export time And if a previously assigned code is inactive at finalization, validation flags the items until remapped
Searchable Output with OCR and Text Index
"As a practitioner, I want the export to be fully searchable so that counsel can quickly locate relevant information without manual review of every page."
Description

Ensure all produced documents are text-searchable. Perform OCR on scanned PDFs/images with language selection and quality controls, embed text layers into PDFs, and extract normalized text for non-PDF formats. Build a lightweight index (e.g., JSON-based or open-source search index) and include a simple offline HTML viewer to search within the package. Preserve and export key metadata fields (dates, titles, tags, client/matter) for ingestion by eDiscovery platforms. Respect redactions by excluding or masking redacted text in all extracted content and indexes.

Acceptance Criteria
OCR Language Selection and Embedded PDF Text Layer
Given a batch containing scanned PDFs and image-only pages with specified OCR language(s) and quality settings When the export runs Then 100% of produced PDF pages that lacked text gain an embedded, selectable text layer without changing visual appearance And the OCR language(s) used are recorded per document in the manifest And per-page OCR confidence is computed; pages under 85% average confidence are flagged in the manifest And produced PDFs support Find and text selection in standard PDF viewers
Normalized Text Extraction for Non-PDF Formats
Given a batch containing DOCX, PPTX, XLSX, EML/MSG, and TXT files When the export runs Then a UTF-8 normalized text file is generated per document (and per page where applicable) with Unix line endings And the text preserves logical reading order and excludes binary artifacts and control characters And the text file path and page mapping are recorded in the manifest And filenames/keys include the document's Bates identifier(s)
Offline HTML Viewer with Search Index and Performance Targets
Given the zipped export is extracted on an offline machine When index.html is opened in a supported browser (Chrome, Edge, Firefox, Safari current -1) Then keyword and phrase searches return results across the package in under 1.5 seconds for up to 25,000 pages And results show highlighted snippets and link to the source document and page And client-side filters by date range, client, matter, and tags are available And the viewer makes no external network requests and functions fully offline And the search index size is <= 15% of the total extracted text size
Redaction-Safe Text Extraction and Indexing
Given documents with page-level or field-level redactions applied before export When text extraction, PDF text embedding, and index building occur Then text within redacted regions or fields is excluded from all indexes And extracted text replaces redacted segments with "[REDACTED]" tokens And searching for any known redacted term yields zero hits and no visible snippets And the PDF text layer contains no bytes of the redacted text (verified by text extraction) And the manifest records redaction presence and scope per document/page/field
Metadata Manifest for eDiscovery Ingestion
Given source documents with metadata (dates, titles, tags, client, matter) and derived fields When the export runs Then a manifest is produced in CSV and JSON with schema including BatesStart, BatesEnd, OriginalFilename, RelativePath, DocumentDate (ISO 8601 with timezone), Title, Tags, Client, Matter, MIMEType, PageCount, Language(s), RedactionStatus And all required fields are populated for 100% of documents; values conform to types and formats; JSON validates against the schema And field-level redactions are reflected by masking values in the manifest as "[REDACTED]" where applicable And the manifest can be imported without errors into a standard eDiscovery platform using the provided headers
End-to-End Package Searchability Verification
Given a mixed corpus of at least 500 documents and 5,000 pages spanning scanned PDFs, born-digital PDFs, images, Office, and email When the export package is produced Then 100% of PDFs in the package contain a searchable text layer or an associated text file And 100% of non-PDF documents have a normalized text file referenced in the manifest And a random sample of 50 documents yields expected hits for predefined test terms in both the PDF viewer Find function and the offline HTML viewer And the export log reports zero critical failures; any OCR failures are listed with file identifiers and reasons
Export Configuration Wizard and Presets
"As a user, I want a guided export wizard with reusable presets so that I can configure compliant exports quickly and repeat them without errors."
Description

Provide a guided, multi-step wizard to configure discovery exports: select scope (clients/matters, date ranges, tags, document types), choose Bates options, redaction rules, privilege categories, and output formats. Display a preflight summary with item counts, estimated size, and validation warnings (e.g., missing privilege reasons). Allow saving reusable presets with role-based access, and support one-click re-runs for supplemental productions that append to existing Bates sequences. Run exports asynchronously with progress tracking, notifications on completion, and a history view for past packages with configuration snapshots for reproducibility.

Acceptance Criteria
Scope Selection in Guided Wizard
Given I have Export permission and at least one client/matter exists When I open the Discovery Export Wizard Then Step 1 displays selectors for Clients/Matters (multi-select), Date Range (start/end), Tags (multi-select with search), and Document Types (multi-select) And when I apply any combination of filters Then the Preview Count updates within 3 seconds and matches the count returned by the search API for the same filters And the Next button remains disabled until at least one item is included by the filters And my applied filters persist when navigating back and forward within the wizard
Bates Numbering and Supplemental Sequencing
Given I configure Bates options with a prefix, zero-padding (min 6), and a starting number or select "Append to existing production" When "Append to existing production" is selected and I choose a prior package Then the starting number auto-sets to last_number + 1 and prefix/padding are locked to match the prior package And validation ensures no gaps or duplicates occur across the prior and current packages for all selected items And if no prior package is chosen, enabling "Append" displays a blocking error and disables Next And attempting to change prefix/padding on a supplemental run shows an inline error and prevents proceeding
Redaction Rules Definition and Non-Destructive Application
Given I select redaction rules for structured fields (e.g., SSN, DOB), enable/disable pattern-based PII/PHI detectors, and define page-level redaction zones per document type When I save the redaction configuration in the wizard Then the Preflight displays counts of items to be redacted by rule type and clearly indicates that originals remain unmodified And the export applies redactions only to derivative copies and removes searchable text within redacted regions for supported formats And disabling all redaction rules results in a zero redaction count without warnings
Privilege Categories and Mandatory Reasons Validation
Given I define one or more privilege categories (e.g., Attorney-Client, Work Product) And I mark items to withhold under those categories When I attempt to proceed to export Then the wizard requires a Reason for each withheld item or batch rule and highlights any missing reasons And the Export button remains disabled until all missing reasons are provided And the Preflight shows a Privilege Log count equal to the number of withheld items
Preflight Summary and Blocking Warnings
Given my export configuration is complete When I open the Preflight step Then I see a summary including: total included items, items to be redacted, withheld items, estimated ZIP size (within ±10% for packages up to 10 GB), selected Bates settings, and selected output formats And a Validation panel lists all warnings and errors with links back to the relevant wizard step And if any blocking validation exists (e.g., missing privilege reasons, invalid Bates range), the Export button is disabled until resolved And clicking View Details reveals sampled item IDs for each category
Preset Save, Role-Based Access, and One-Click Re-Run
Given I have permission to manage presets When I save the current configuration as a Preset with a unique name and assign roles (Owner, Admin, Staff) Then the Preset becomes available in the Preset dropdown to assignees within 5 seconds and is hidden from non-assignees And loading a Preset hydrates all wizard steps, including scope filters, Bates options, redaction rules, privilege settings, and output formats And from a prior export's History entry, clicking "Re-run as Supplemental" opens the wizard with that snapshot loaded and "Append to existing Bates" pre-selected And re-running creates a new package that appends Bates without duplicating previously produced items
Asynchronous Export, Notifications, and Reproducible History
Given I click Export on the Preflight step Then a background job is created with statuses: Queued, Running, Completed, Failed And the UI shows progress percentage and item counts updated at least every 5 seconds And on completion or failure, I receive an in-app notification and an email with a link to the History entry And the History view lists: timestamp, user, scope snapshot, Bates settings, redaction and privilege configurations, item counts, final ZIP size, checksums, and a signed manifest download And the configuration snapshot is immutable (read-only) and can be cloned into a new preset for edits
Audit Trail and Chain-of-Custody Tracking
"As a compliance officer, I want a full chain-of-custody audit trail so that I can demonstrate who exported what, when, and under which controls."
Description

Record a comprehensive audit trail for each export: requester identity, MFA status, IP/device, timestamps for each stage, selected scope, configuration hash, and manifest signature details. Log download/access events and optionally require expiring, access-controlled download links. Embed a chain-of-custody report in the package and store an immutable copy in SoloPilot’s audit logs for compliance. Prevent tampering by hashing audit records and restricting deletion to retention policies. Provide an API endpoint and admin view to retrieve audit evidence for court or compliance inquiries.

Acceptance Criteria
Export Audit Trail Captures Requester and Environment
Given a signed-in user initiates a Discovery Export And MFA is enforced for the workspace When the user completes MFA and submits the export Then the audit record stores requester userId, email, role, MFA status (pass/fail), factor type, IP address, user agent or device fingerprint, and request timestamp in ISO 8601 with timezone And each subsequent stage writes an ISO 8601 timestamp: queuedAt, startedAt, redactionCompletedAt (if applicable), manifestSignedAt, finishedAt And the audit record is assigned a unique exportId as a UUID v4 and is retrievable by exportId
Configuration Hash and Manifest Signature Recorded
Given an export is created with a defined scope and settings When the export job is persisted Then the audit record includes a normalized JSON snapshot of selected scope and settings (matters, date range, filters, redaction rules, privilege log settings) And a SHA-256 hash of that snapshot labeled configHash And the signed manifest records algorithm, signer identity, signature timestamp, and signature value And the configHash is embedded in the manifest and chain-of-custody report And public-key verification of the manifest signature succeeds using the published key
Immutable Hash-Chained Audit Storage and Retention Controls
Given an audit record is written When the record is committed to the audit log store Then the record is assigned contentHash (SHA-256 over a canonicalized record) and prevHash linking to the prior committed record to form a hash chain And attempts to modify or delete any committed record via UI or API return 403 Forbidden and are logged And deletion occurs only via automatic retention expiration and emits a retentionDeletion event And chain verification detects any tampering and exposes failures in the admin view and API
Download and Access Event Logging with Expiring Links
Given an export is marked finished And access-controlled links are required by organization policy When an admin generates a download link with expiry=72h and maxDownloads=2 Then the system issues a signed URL bound to exportId, requester/admin userId, optional IP range, and expiration And the link becomes invalid after expiry or after two successful downloads, returning 410 Gone on subsequent attempts And every download attempt logs who, when, IP, user agent, linkId, and outcome (success/expired/denied) And the first successful download sets firstDownloadedAt in the audit record
Chain-of-Custody Report Embedded in Export Package
Given an export completes successfully When the package is assembled Then the package includes a chain_of_custody.json and chain_of_custody.pdf And the report lists each custody event with timestamps: creation, queue, processing, redaction, signing, link creation, and downloads And the report contains configHash, contentHash, prevHash, manifest signature metadata, and per-file checksums And recomputing checksums over the delivered package matches the values in the report
API Endpoint Returns Audit Evidence for Compliance
Given an authenticated caller with role Admin or Compliance invokes GET /api/audit/exports/{exportId} When the request includes a valid token Then the API responds 200 with JSON fields: exportId, requester, MFA, IP/device, timestamps, scope snapshot, configHash, manifest signature, hashChain fields, and download events[] And 404 is returned for unknown exportId, 403 for unauthorized access, and 422 for malformed identifiers And the API supports filtering via GET /api/audit/exports?from=...&to=...&requester=... And responses are read-only and include ETag and Cache-Control: no-store
Admin View Displays, Filters, Verifies, and Exports Audit Evidence
Given an admin opens the Discovery Export audit view When they filter by date range, requester, or exportId Then results load within 2 seconds for up to 10,000 records using server-side pagination And selecting an export shows all audit fields, signature verification status (pass/fail), and a Copy for court export (PDF/JSON) identical to the API output And the UI Verify integrity action recomputes the hash chain and returns a result within 5 seconds for 1,000 records

Product Ideas

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

Inbox-to-Booking Bridge

Convert email or DM threads into bookings with one click. Auto-create contacts, pull availability, and attach required intake forms.

Idea

Credit Candle

Track prepaid session credits as a visual burn-down. Send low-credit alerts and auto-bill overages the moment a session exceeds the plan.

Idea

No-Show Shield

Enforce deposits, SMS nudges, and cancellation fees automatically. One-tap waiver overrides policies when you choose leniency.

Idea

Retainer Radar

Track retainer usage against allowances in real time. Forecast overages mid-cycle and schedule top-up invoices before surprises hit.

Idea

Timezone Twin Display

Show both parties’ local times on every booking, reminder, and invoice. Block off-hour slots and misfires with clear side-by-side clocks.

Idea

Workshop Rollcall Paylinks

Mark attendance during sessions and auto-send per-attendee paylinks. Bulk reconcile payments and issue straggler reminders in one sweep.

Idea

Compliance Vault

Store notes in tamper-evident WORM storage with full audit trails. Export a time-stamped compliance binder in one click.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

SoloPilot Launches Inbox-to-Booking Suite to End Scheduling Back-and-Forth for Independent Pros

Imagined Press Article

San Francisco, CA — SoloPilot, the lightweight practice‑management platform built for independent consultants, coaches, therapists, and freelancers, today announced the general availability of its Inbox‑to‑Booking Suite—a cohesive set of automation features that turns first-contact messages into confirmed sessions in a single reply. The new suite unifies email and DM intent detection, live time options, reply-to-book confirmations, smart document attachments, and end-to-end thread synchronization, eliminating the copy‑paste chaos that drains billable hours and delays revenue. The launch bundles several capabilities designed to work together from the first outreach: Intent Detect reads inbound messages to identify scheduling intent and extract the who/what/when; Inline Slots inserts live, clickable time options into replies; Reply‑to‑Book lets clients confirm by simply replying in natural language; Signature Enrich auto‑creates complete contacts from email signatures and social profiles; Auto‑Form Picker attaches the right intake forms and policies; Nudge Sequencer automates polite follow‑ups; and Thread Sync links the entire conversation to the booking, notes, and invoice for a single, searchable timeline. “Independents shouldn’t lose a third of a morning to ‘Does Tuesday 3 pm work?’ ping‑pong,” said Maya Chen, founder and CEO of SoloPilot. “Our Inbox‑to‑Booking Suite makes the first reply do all the heavy lifting—offering live times in the client’s timezone, attaching the right forms, and confirming the session when they say ‘yes.’ Paired with SoloPilot’s one‑click session‑to‑invoice flow, practitioners routinely recover six or more billable hours every month.” For Spreadsheet Migrators leaving manual tools and ad‑hoc workflows, the suite offers a fast path from chaotic inquiry threads to structured, billable appointments. Session Sprinters who run back‑to‑back meetings gain a frictionless way to keep momentum between sessions without opening separate schedulers or CRMs. Compliance Keepers benefit from accurate contact profiles, policy attachments, and a clean audit trail that persists from the first message to the final invoice. “Before SoloPilot, I was juggling DMs, email, and a calendar widget that confused half my clients,” said Imani Adeyemi, an executive coach and early adopter. “Now my first reply includes localized times the client can tap, and when they answer ‘Let’s do 3 pm,’ SoloPilot books it, sends my pre‑work questionnaire, and creates the contact with phone, company, and timezone. I stopped retyping the same details and stopped missing follow‑ups.” Under the hood, SoloPilot’s Inline Slots respects each recipient’s timezone automatically and checks real‑time availability against the practitioner’s calendar, preventing double-booking. Reply‑to‑Book uses natural language understanding to interpret confirmations or clarifying questions, triggering a confirmation sequence that includes calendar invites, intake forms, and payment or deposit requests when applicable. When threads stall, Nudge Sequencer sends branded, polite reminders at smart intervals via the same channel—email, SMS, or DM—refreshing the available times without making the practitioner re-compose a message. “From the first contact, the system steadily reduces friction and surfaces the right guardrails,” said Ravi Patel, Head of Product at SoloPilot. “Signature Enrich builds accurate records for billing and reminders. Auto‑Form Picker attaches NDAs or intake forms based on context. Thread Sync gives a single source of truth that follows the engagement—from inquiry to notes to invoice—so nothing is lost and nothing needs to be copied between tools.” The Inbox‑to‑Booking Suite dovetails with SoloPilot’s core promise: one‑click session‑to‑invoice that auto‑populates notes and billing details, preventing missed charges and accelerating payments. With booking and intake happening in the natural flow of conversation, practitioners can track the engagement end‑to‑end, reconcile faster, and maintain a clear record for audits or disputes. The combined workflow helps SoloPilot users reduce days sales outstanding, reclaim time previously lost to manual handoffs, and present a more professional, consistent client experience. Early results from the beta indicate meaningful business impact: faster time‑to‑first‑session for new inquiries, elevated show rates thanks to timely nudges and confirmations, and higher conversion from inquiry to booked appointment. For independents in growth mode, this improves revenue predictability; for practitioners balancing client work after hours, it means fewer late‑night admin tasks and fewer opportunities for leads to go cold. Availability and pricing: The Inbox‑to‑Booking Suite is available today to all SoloPilot plans at no additional cost during the launch window. Advanced configuration for form rules and channel sequencing is included in Pro tiers. New users can start a free trial and import existing contacts and calendar data in minutes; existing customers will find the new capabilities enabled in their Messaging and Scheduling settings. Call to action: To see the Inbox‑to‑Booking Suite in action or to start a free trial, visit solopilot.app/inbox-to-booking. About SoloPilot: SoloPilot is a lightweight practice‑management SaaS that centralizes scheduling, client notes, invoicing, and automations into one workspace. Built for independent consultants, coaches, therapists, and freelancers, SoloPilot streamlines the entire engagement lifecycle—from first contact to final payment—so practitioners can stop manual handoffs, prevent missed charges, and reclaim six or more billable hours each month. Media contact: - Press: press@solopilot.com - Media kit and product images: solopilot.app/press - Phone: +1 (415) 555‑0136 Forward‑looking statements: This press release contains forward‑looking statements about product capabilities and expected benefits. Actual results may differ. SoloPilot reserves the right to modify features and pricing at any time.

P

SoloPilot Unveils Retainer Automation Stack for Predictable Cash Flow and Zero‑Spreadsheet Reconciliations

Imagined Press Article

San Francisco, CA — SoloPilot today announced the Retainer Automation Stack, a comprehensive set of features that gives independent consultants and agencies enterprise‑grade control over monthly retainers without spreadsheet gymnastics. The stack automates allowance tracking, mid‑cycle forecasts, proration, end‑of‑cycle true‑ups, client transparency, and budget caps—delivering predictable cash flow and audit‑ready clarity for both sides. The Retainer Automation Stack weaves together Cycle True‑Up, Top‑Up Orchestrator, Client Usage Portal, Proration Wizard, Contract Router, Underuse Rescue, and PO Cap Guard. Together, these features replace manual reconciliations and guesswork with real‑time usage visibility, proactive mid‑cycle actions, and crisp client communications that forestall month‑end disputes. “Retainers are where good intentions go to die when they’re managed in spreadsheets,” said Maya Chen, founder and CEO of SoloPilot. “Our Retainer Automation Stack ensures every cycle closes cleanly—usage tallies correctly, overage or underuse is applied according to your rules, and clients see a transparent statement the minute the period ends. It’s a serious upgrade for anyone who bills by allowance, whether that’s hours, credits, or deliverables.” With Cycle True‑Up, SoloPilot automatically reconciles allowances at cycle end—calculating usage, applying carryover or expiry rules, and generating the correct invoice or credit memo. Top‑Up Orchestrator monitors Depletion Forecasts mid‑cycle and, when a shortfall is predicted, schedules and sends top‑up invoices—optionally requiring client pre‑approval—to keep delivery unblocked. The Client Usage Portal gives stakeholders a live view of allowance consumed, upcoming sessions, projected run‑out date, and invoices due, complete with embedded action buttons for approvals or top‑ups. “Clients don’t escalate when they’re never surprised,” said Ravi Patel, Head of Product at SoloPilot. “The portal, automated statements, and clear carryover rules take the drama out of month‑end. Consultants can stop negotiating from memory and start collaborating on what work to prioritize next.” Contract Router auto‑assigns sessions, notes, and invoices to the correct retainer based on project, tags, or service, prompting confirmation when rules conflict. Proration Wizard makes onboarding and scope changes painless by accurately prorating allowances and invoices when a retainer starts, pauses, or changes mid‑month. Underuse Rescue detects underutilization early and recommends actions—priority booking slots, friendly nudges, or limited rollover offers—to help clients realize full value before cycle end, protecting retention and converting idle capacity into scheduled work. PO Cap Guard enforces purchase order or budget ceilings by alerting consultants as limits approach and automatically pausing over‑cap auto‑billing, while sending clients prefilled extension requests or top‑up options to maintain compliance. For Retainer Trackers and Cashflow Accelerators, the stack translates directly into fewer write‑offs and more predictable payouts. Independent practitioners managing multiple workstreams will appreciate Split Ledger compatibility and a clean audit trail that threads usage, invoices, and approvals across the life of each contract. Combined with SoloPilot’s one‑click session‑to‑invoice, the result is an end‑to‑end pipeline that prevents leakage and speeds cash collection. “Last quarter, I spent two days reconciling a three‑project client,” said Theo Müller, a globetrotting consultant who participated in the beta. “Now Cycle True‑Up closes the books for me, and the Client Usage Portal means finance has zero ‘Can you screenshot your hours?’ emails. When we hit 80% mid‑month, Top‑Up Orchestrator proposes options and gets sign‑off in hours, not weeks.” The Retainer Automation Stack is designed to be flexible across engagement models—time banks, credit packs, deliverable quotas, or hybrid structures. Practitioners set rules once in Package Rules and let SoloPilot enforce them across scheduling, notes, and invoicing. When coupled with Overage Autopilot and Top‑Up Paylinks, overages are billed the moment usage exceeds the plan, with clear line items and client‑friendly explanations that reduce disputes. Availability and pricing: The Retainer Automation Stack is available today for Pro and Business plans, with Client Usage Portal access included for all retainers at no extra cost. Existing customers can enable the stack in Settings → Billing & Retainers. New users can start a free trial and import current retainers without losing historical context. Call to action: Explore the Retainer Automation Stack and see a guided demo at solopilot.app/retainers. About SoloPilot: SoloPilot is a lightweight practice‑management SaaS that centralizes scheduling, client notes, invoicing, and automations into one workspace. It helps independents stop manual handoffs with one‑click session‑to‑invoice, reclaiming six or more billable hours each month, preventing missed charges, and accelerating payments. Media contact: - Press: press@solopilot.com - Media kit and product images: solopilot.app/press - Phone: +1 (415) 555‑0136 Forward‑looking statements: This press release contains forward‑looking statements. Features and pricing are subject to change.

P

SoloPilot Introduces No‑Show Revenue Protection to Safeguard Solo Practice Calendars and Cash Flow

Imagined Press Article

San Francisco, CA — SoloPilot today unveiled No‑Show Revenue Protection, a coordinated set of booking‑stage and pre‑session safeguards that boost attendance, fairly enforce policies, and recover revenue without awkward chases. The system combines Risk Deposit Engine, Confirm‑to‑Keep, Waitlist Backfill, Fee Escalator, Waiver Ledger, and Card Vault to deliver a calm, consistent client experience that keeps calendars accurate and cash flow predictable. Missed and late‑canceled appointments quietly erode independent income. They also create friction if handled inconsistently. SoloPilot’s new capabilities automate the right actions at the right time—sizing deposits by risk, nudging clients to confirm, releasing unconfirmed slots, backfilling openings from a waitlist, enforcing graduated fees with clear messaging, and capturing payment instantly when policies apply. “Practitioners want to be generous with good clients and firm with repeat no‑shows—without turning into debt collectors,” said Maya Chen, founder and CEO of SoloPilot. “Our No‑Show Revenue Protection respects that balance. Policies are communicated upfront, confirmations are effortless, and charges happen only when the rules say they should—with a transparent breakdown the client can understand.” At booking, Card Vault securely collects a payment method and runs smart pre‑authorizations, enabling instant capture for no‑shows and late cancels. The Risk Deposit Engine sizes deposits by client history, time slot, and service type so high‑risk slots request a higher hold while reliable clients see lighter asks; deposits auto‑convert to session credit upon attendance or forfeit per policy on late cancel/no‑show. Confirm‑to‑Keep sends timed SMS/email check‑ins (for example, 24 hours and 2 hours before the session) with one‑tap Confirm or Reschedule. Unconfirmed appointments auto‑release at the configured cutoff and offer a fee‑free reschedule within the grace window. When a slot opens, Waitlist Backfill immediately pings interested clients in priority order with a one‑tap Claim link that collects the deposit and updates the calendar in minutes. For cancellations inside the policy window, Fee Escalator enforces a clear, graduated fee schedule based on lead time—pre‑authorizing up to the maximum fee and capturing the correct amount automatically, with transparent line items on the invoice. Waiver Ledger gives practitioners a humane override: one‑tap waivers with a reason picker, private notes, and caps on how often waivers can be used per client, preserving fairness while providing flexibility. “I work with a flaky client base,” said Nova Reyes, a performance coach who participated in the pilot. “Before SoloPilot, I was either chasing deposits or eating the loss. Now clients get a friendly confirm prompt and know the policy. If they don’t confirm, the slot goes to my waitlist. I still have discretion with Waiver Ledger, but I’m not negotiating every case in my DMs.” For therapists and regulated coaches, consistent enforcement pairs with clear documentation. SoloPilot’s Thread Sync links confirmations, nudges, and policy messages to the booking’s audit trail, simplifying disputes and insurance documentation when needed. Automatic receipts include Dual‑Time Stamps showing both parties’ local time and UTC for unambiguous records across time zones. “Reducing no‑shows isn’t only about fees,” added Ravi Patel, Head of Product at SoloPilot. “It’s about clarity and timing. When clients get a respectful nudge at the right hour, they confirm. When life happens, an easy reschedule beats a missed appointment. And when policies apply, the invoice is already prepared with the exact breakdown—no spreadsheets, no stress.” No‑Show Revenue Protection integrates directly with SoloPilot’s one‑click session‑to‑invoice workflow. Once a session completes, notes and billing auto‑populate; when a session is missed or late‑canceled, the correct fee and explanation flow onto the invoice automatically. The net result: fewer admin hours, higher show rates, and recovered revenue without awkward back‑and‑forth. Availability and configuration: No‑Show Revenue Protection is available today across all SoloPilot plans. Risk‑based deposit rules, confirmation cadences, waitlist priority logic, and fee schedules are fully configurable by service type. Card Vault supports major cards and auto‑updates expired cards; Apple Pay and Google Pay are supported for client‑initiated paylinks. Call to action: Learn how to set up No‑Show Revenue Protection in under ten minutes at solopilot.app/no-show-protection and start a free trial. About SoloPilot: SoloPilot is a lightweight practice‑management SaaS that centralizes scheduling, client notes, invoicing, and automations into one workspace. Built for independent consultants, coaches, therapists, and freelancers, SoloPilot replaces manual handoffs with one‑click session‑to‑invoice—reclaiming six or more billable hours monthly, preventing missed charges, and accelerating payments. Media contact: - Press: press@solopilot.com - Media kit and product images: solopilot.app/press - Phone: +1 (415) 555‑0136 Forward‑looking statements: This press release may include forward‑looking statements. Actual results could differ. Features and pricing are subject to change.

P

SoloPilot Rolls Out Global Time Intelligence to Eliminate Cross‑Time‑Zone Scheduling Errors

Imagined Press Article

San Francisco, CA — SoloPilot today announced Global Time Intelligence, a suite of scheduling and billing capabilities designed to remove cross‑time‑zone confusion for independent professionals and their clients. The release includes Twin Clocks Overlay, Respectful Hours Guard, DST Guardian, TravelSense Profiles, Overlap Heatmap, and Dual‑Time Stamps—tools that make it effortless to propose fair meeting times, communicate clearly across borders, and maintain audit‑friendly records. Working across time zones shouldn’t require arithmetic gymnastics or apology emails after daylight‑saving switches. Yet independents often lose hours to rescheduling, missed calls, and invoice disputes that boil down to “Whose 3 pm was that?” Global Time Intelligence addresses the problem at its source: every time a time appears in SoloPilot, it is contextualized for both parties—during booking, in reminders, inside notes, and on invoices. “Cross‑border business is the norm for many independents, but the tools they rely on still assume a single time zone,” said Maya Chen, founder and CEO of SoloPilot. “We designed Global Time Intelligence to be the context layer that never forgets daylight saving rules, travel windows, or local business hours—so you can propose better times faster and show up on time, every time.” Twin Clocks Overlay displays side‑by‑side local times for you and your client anywhere a time appears, with hover details for timezone codes and DST status. Respectful Hours Guard automatically blocks off‑hour booking and message send times based on each party’s preferred windows, suggesting the nearest mutually respectful options to reduce friction and boost response rates. DST Guardian monitors upcoming daylight‑saving shifts for both parties and keeps recurring appointments pinned to each person’s intended local time, sending proactive heads‑up notices with one‑tap rebase for all affected sessions. For traveling clients and practitioners, TravelSense Profiles detect temporary timezone changes from calendar locations, email signatures, or user prompts. During travel windows, booking displays and reminder timing adjust automatically, preventing accidental early or late arrivals. For multi‑party scheduling, Overlap Heatmap visualizes the best meeting overlaps across two or more time zones with a color‑coded grid and fairness rotation, enabling one‑tap proposals that localize times for each invitee. Dual‑Time Stamps add clear timestamps to invoices, receipts, and session summaries showing both parties’ local time, timezone abbreviation, and UTC—a boon for compliance exports and cross‑border finance teams. “As a consultant with clients across five time zones and two currencies, I can’t afford time math errors,” said Theo Martín, a strategy consultant who joined the beta. “Twin Clocks Overlay tells me at a glance whether my Friday 8 am is their Friday or their Saturday, and DST Guardian saved a recurring series during the spring forward. It’s the peace of mind I didn’t know I needed.” The new suite integrates with SoloPilot’s Inbox‑to‑Booking tools. Inline Slots automatically adjusts to the recipient’s timezone; Reply‑to‑Book interprets confirmations like ‘Tuesday 3 pm works’ correctly for both parties; and Nudge Sequencer schedules reminders to land at sane local hours via Respectful Hours Guard. With Thread Sync, the original conversation and all subsequent scheduling actions remain attached to the booking, ensuring a clean timeline of decisions—a key advantage for regulated engagements and enterprise clients. “Time clarity is conversion,” added Ravi Patel, Head of Product at SoloPilot. “When prospects see options that respect their local hours, they book. When recurring series survive DST, you avoid churn. And when invoices show both parties’ local time plus UTC, disputes have nowhere to hide.” Global Time Intelligence supports SoloPilot’s mission to reclaim billable hours by removing the low‑grade friction of scheduling and reconciliation. With combined tooling that proposes fair times, honors local hours, anticipates DST shifts, and records events precisely, independents can spend more energy on delivery—and less on apology notes or spreadsheet side‑calculations. Availability and pricing: Global Time Intelligence is available today to all SoloPilot customers. Fair‑hours policies, DST handling, and travel windows are configurable per service and per client. Overlap Heatmap and fairness rotation are included on Pro and Business plans; all other features are available across tiers. Call to action: See Global Time Intelligence in action and launch a free trial at solopilot.app/time. About SoloPilot: SoloPilot is a lightweight practice‑management SaaS that centralizes scheduling, client notes, invoicing, and automations into one workspace. It helps independent consultants, coaches, therapists, and freelancers stop manual handoffs with one‑click session‑to‑invoice—reclaiming six or more billable hours monthly, preventing missed charges, and accelerating payments. Media contact: - Press: press@solopilot.com - Media kit and product images: solopilot.app/press - Phone: +1 (415) 555‑0136 Forward‑looking statements: This press release may contain forward‑looking statements. Actual results may vary. Features and pricing are subject to change.

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.