Nonprofit CRM and volunteer management

GiveCrew

Effortless coordination, visible impact

GiveCrew is a lightweight nonprofit CRM for grassroots organizers, part-time staff, and rotating volunteers with no IT support. It replaces spreadsheets and scattered apps with one mobile workflow that captures signups, donations, and shift assignments, auto-sends receipts and reminders, and fills open slots. Pilots cut admin time 58% and lift volunteer show-up 22%. A live Impact Board generates progress posters.

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

GiveCrew

Product Details

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

Vision & Mission

Vision
Mobilize every grassroots nonprofit to transform community energy into measurable impact through effortless coordination and joyful giving and volunteering.
Long Term Goal
By 2029, equip 25,000 grassroots chapters worldwide to save 1 million admin hours annually and lift volunteer retention 30%, turning community energy into measurable action.
Impact
In early pilots, grassroots nonprofits replacing spreadsheets cut admin time 58% per event, increase volunteer show-up 22%, and send donation receipts 17% faster, enabling small teams without IT support to reallocate hours to outreach and improve year-over-year donor retention.

Problem & Solution

Problem Statement
Grassroots nonprofit organizers with rotating volunteers juggle donors, signups, and event shifts across spreadsheets and scattered apps, causing missed signups, delayed receipts, and poor follow-up. Existing CRMs are bloated, costly, and IT-heavy, failing teams without IT support.
Solution Overview
GiveCrew replaces spreadsheets and scattered apps with a single mobile workflow that captures signups, donations, and shift assignments in one pass. Auto-matching fills open slots and triggers instant receipts and reminders, while a live Impact Board generates a shareable progress poster that rallies donors and volunteers.

Details & Audience

Description
GiveCrew is a lightweight nonprofit CRM that unifies donations, volunteers, and events in one clean workflow. Built for grassroots organizers, part-time staff, and rotating volunteers with no IT support. It replaces spreadsheets and scattered apps, automates signups, receipts, and reminders, cutting admin time 58% and boosting volunteer show-up 22%. Its live Impact Board auto-generates shareable progress posters that rally donors and recruit volunteers.
Target Audience
Grassroots nonprofit organizers (20-60) juggling donors and volunteers, replacing spreadsheets; mobile-first, no IT support.
Inspiration
At a Saturday park cleanup, the organizer clutched a clipboard, three QR codes flapping in the wind, and a buzzing phone while volunteers lined up. Half the names smeared; receipts would be sent later. A teen paused, trash grabber in hand: Where do I see progress? That moment sparked GiveCrew: a single, mobile flow from signup to shift to donation, with a live Impact Board that makes progress visible now.

User Personas

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

C

Coalition Connector Camila

- Age 29–42; community partnerships lead or volunteer coordinator. - Urban coalition setting; works evenings and weekends. - Bachelor’s in sociology/public policy; bilingual Spanish–English. - Mobile-first; comfortable with Slack, WhatsApp, and basic CRMs.

Background

Started as a neighborhood canvasser, then brokered collaborations among overlapping nonprofits. After a fiasco with duplicate invites tanked turnout, she vowed to centralize partner attribution. She now prioritizes tools that make sharing easy and credit clear.

Needs & Pain Points

Needs

1. One-click cross-org share with dedupe. 2. Clear partner attribution on signups/donations. 3. Auto-updated co-host rosters and slots.

Pain Points

1. Duplicate invites depress turnout and trust. 2. Unclear credit strains partner relationships. 3. Manual coordination consumes late evenings.

Psychographics

- Relationships first; tools stay out of way. - Craves shared wins and public accountability. - Values reciprocity, transparency, fast follow-through. - Hates bureaucracy; prefers lightweight mobile workflows.

Channels

1. WhatsApp — partner groups 2. Gmail — intros 3. Slack — coalition workspace 4. Facebook Groups — community posts 5. SMS — confirmations

T

Text-to-Give Talia

- Age 24–38; field fundraiser or lead canvasser. - Suburban swing districts; evenings and weekends. - Comfortable with mobile payments; iOS and Android. - Income supplemented by stipends; budget-conscious tools.

Background

Began as a volunteer tabler, losing donors to clunky forms and dead wifi. After piloting QR codes, she sought a smoother flow with offline capture and instant receipts. Now she tunes nudges to convert one-time givers into sustainers.

Needs & Pain Points

Needs

1. Fast SMS/QR donations with auto-receipts. 2. One-tap monthly sustainer upsell. 3. Live totals to energize crowds.

Pain Points

1. Spotty wifi breaks donation forms. 2. Missing receipts trigger donor complaints. 3. Extra taps kill conversion rates.

Psychographics

- Thrives on momentum and instant gratification. - Believes tiny gifts compound into power. - Wants clear totals and progress thermometers. - Prefers concise SMS over lengthy emails.

Channels

1. SMS — donation prompts 2. Instagram Stories — live asks 3. WhatsApp — volunteer groups 4. Square — payment links 5. Gmail — donor follow-ups

R

Remote Roster Rahul

- Age 27–45; remote volunteer coordinator. - Works from home; evenings across time zones. - Tech-savvy; dual monitors, fast internet. - Manages 100–300 rotating volunteers.

Background

Former product manager who replaced ad hoc Airtables with structured playbooks. After repeated no-shows sunk goals, he doubled down on layered reminders and simple check-ins. He now seeks one screen to assign, nudge, and measure.

Needs & Pain Points

Needs

1. Time-zone aware shift scheduling. 2. Multi-channel reminder cadences with escalation. 3. One-screen check-ins and no-show recovery.

Pain Points

1. Volunteers ghost without layered nudges. 2. Time-zone math causes scheduling errors. 3. Tool switching fragments focus.

Psychographics

- Obsessed with reliable turnout over heroics. - Data-driven; dashboards beat anecdotes every time. - Values autonomy and asynchronous collaboration. - Minimizes meetings; loves crisp playbooks.

Channels

1. Slack — volunteer channels 2. Zoom — bank rooms 3. Discord — late-night chat 4. SMS — reminders 5. Gmail — onboarding packets

C

Campus Catalyst Kai

- Age 19–23; undergraduate organizer. - Mid-size university; commuter and residential mix. - Android-heavy friend group; limited budgets. - Rotating officers; shared admin responsibilities.

Background

Learned logistics running orientation crews, then inherited a chaos of Google Forms and GroupMe threads. After leaders graduated, everything reset. Kai now favors repeatable templates and links that live in bios.

Needs & Pain Points

Needs

1. Tap-to-join from Instagram bio. 2. Class-aware reminder timing controls. 3. Easy officer handoff of admin rights.

Pain Points

1. Signups buried in DMs and threads. 2. Leadership turnover erases knowledge. 3. Clunky forms scare off friends.

Psychographics

- Energized by streaks and friendly competition. - Wants tools that feel fun, lightweight. - Values speed, clarity, zero-friction joins. - Skeptical of admin-heavy enterprise systems.

Channels

1. Instagram — bio link 2. GroupMe — chapter chat 3. Discord — planning server 4. TikTok — calls 5. Gmail — faculty approvals

A

Accessibility Ally Alex

- Age 30–50; inclusion coordinator or lead volunteer. - Metro area with multilingual neighborhoods. - Bilingual; comfortable with assistive tech basics. - Moderate tech confidence; mobile-first outreach.

Background

Ran a multilingual mutual-aid hotline during the pandemic and watched English-only emails miss people. After fixing form contrast and adding SMS, attendance jumped. Alex now bakes accessibility into every campaign plan.

Needs & Pain Points

Needs

1. SMS-capable signups with language toggle. 2. Accommodation flags and private notes. 3. Contrast-safe, alt-text-ready templates.

Pain Points

1. Forms break on older phones. 2. No place to capture accommodations. 3. English-only reminders exclude neighbors.

Psychographics

- Inclusion is mandatory, never optional. - Prefers SMS-first, low-bandwidth experiences. - Values privacy around accommodations data. - Champions multilingual outreach by default.

Channels

1. SMS — primary channel 2. WhatsApp — multilingual groups 3. Facebook — community pages 4. YouTube — captioned updates 5. Gmail — coordination

R

Rapid Response Rina

- Age 26–44; mutual aid dispatcher. - Coastal city with frequent weather events. - Irregular hours; prepaid phones common. - Minimal IT support; paper backups.

Background

Coordinated hurricane supply runs using paper logs and SMS trees. Lost clipboards taught her the cost of scattered tools. She now insists on one mobile workflow with offline capture and printable summaries.

Needs & Pain Points

Needs

1. Offline signups and check-ins with sync. 2. One-tap fills for last-minute cancellations. 3. Printable Impact Board status posters.

Pain Points

1. Outages stall forms and check-ins. 2. Cancellations create dangerous coverage gaps. 3. Paper records get lost.

Psychographics

- Calm under pressure, urgency with care. - Reliability over features, always. - Mission-first; zero patience for bureaucracy. - Prefers tools anyone grasps immediately.

Channels

1. SMS — dispatch alerts 2. WhatsApp — neighborhood groups 3. Zello — voice coordination 4. Facebook — community pages 5. Gmail — agency liaison

Product Features

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

Smart Radius

Auto-sizes the geofence based on urgency, volunteer density, and time-to-shift. Captains can boost or tighten with one tap. Cuts noise by only messaging people who can actually make it, filling gaps faster with fewer pings.

Requirements

Dynamic Geofence Engine
"As a field captain, I want the app to auto-size the outreach area for my shift so that I only ping volunteers who can realistically make it in time."
Description

Automatically calculates an optimal outreach radius for each shift using inputs such as shift urgency (time-to-start, fill rate), volunteer density (historical active volunteers nearby), and time-to-shift windows. Produces a distance radius or travel-time isochrone, recalculating on key events (shift creation, T-24/T-6/T-1, slot changes, manual boost/tighten). Provides configurable org-level defaults and min/max radius caps. Integrates with shift scheduler and volunteer directory to read location and availability data. Computes results within one second and safely falls back to the last known radius if external services fail.

Acceptance Criteria
Compute Optimal Radius on Shift Creation
Given an org with configured defaults and caps and a new shift with start time, location, slot count, and current fill rate When the engine processes the shift creation event Then it computes an outreach boundary using urgency (time-to-start, fill rate), volunteer density (historical active volunteers nearby), and time-to-shift windows And it produces the boundary in the configured mode (distance radius or travel-time isochrone) And the result is persisted to the shift record and exposed via scheduler API within 1 second (p95) And the boundary size respects the org-level min and max caps
Timed Recalculation at T-24/T-6/T-1
Given a scheduled shift with a previously computed boundary When system time reaches T-24h, T-6h, or T-1h before shift start Then the engine recalculates the boundary using current inputs And the updated boundary is available via API within 1 second (p95) of the trigger And if inputs have not materially changed, the boundary remains unchanged and no duplicate update event is emitted
Recalculation on Slot Changes
Given a shift with N open slots and an existing boundary When open slots or fill rate changes due to signups or cancellations Then the engine recalculates the boundary within 1 second (p95) of the change And the resulting boundary expands when urgency increases and contracts when urgency decreases, bounded by org min/max caps
Captain Boost/Tighten One-Tap Control
Given a captain with permission views a shift with a computed boundary When the captain taps Boost or Tighten Then the engine applies the configured boost/tighten factor to expand/contract the boundary And the updated boundary is computed, persisted, and visible in the scheduler within 1 second (p95) And the boundary never exceeds the org max or drops below the org min
Output Mode Selection: Distance vs Isochrone
Given the org boundary mode is set to Distance or Travel-Time When the engine computes a boundary Then in Distance mode, it returns a circular geometry (center: shift location) with radius in kilometers And in Travel-Time mode, it returns an isochrone polygon for the configured travel profile and time budget And the response includes mode, units, geometry, generatedAt timestamp, and version fields
Integration with Scheduler and Volunteer Directory
Given the scheduler provides shift metadata and the directory provides volunteer locations and activity recency When the engine computes volunteer density around the shift location Then it includes only volunteers marked active within the configurable recency window And it excludes volunteers with conflicting availability or do-not-contact flags And missing or invalid volunteer locations are ignored without failing the computation
External Service Failure Failover
Given the engine depends on external geospatial/routing services When those services time out or return an error during computation Then the engine returns the last known boundary for the shift within 1 second (p95) And if no prior boundary exists, it returns the org default boundary within configured caps And the failure is recorded and a background retry is scheduled per backoff policy
Travel-Time Eligibility
"As a volunteer, I want to be contacted only when I can get to a shift on time so that I’m not asked to do something I can’t make."
Description

Evaluates each volunteer’s eligibility based on current travel-time estimates from their chosen location (home, last check-in, or approximate area) to the shift site, considering live traffic and mode preferences. Marks volunteers eligible if their ETA fits configurable arrival buffers (e.g., arrive ≥10 minutes before start). Degrades gracefully to straight-line distance thresholds when travel-time data is unavailable, with caching and rate limiting to control API usage. Supports approximate locations (e.g., ZIP centroid) for privacy and integrates with location settings.

Acceptance Criteria
Eligibility From Chosen Location with Live Traffic
Given a volunteer has chosen location = Home (coordinates available) and mode = Driving And a shift at Site A with start time 18:00 and arrival buffer = 10 minutes When the live-traffic travel-time service returns an ETA arrival time at or before 17:50 from the Home location using Driving Then the volunteer is marked Eligible for the shift And the eligibility record stores source location type = Home, mode = Driving, travel_time_minutes, data_source = LiveTraffic, and computed_at timestamp When the ETA arrival time is after 17:50 Then the volunteer is marked Ineligible for the shift
Arrival Buffer Configurability and Enforcement
Given Shift B has a shift-specific arrival buffer set to 15 minutes And a volunteer’s computed live-traffic ETA is 14 minutes before the shift start Then the volunteer is marked Ineligible When the arrival buffer for Shift B is updated to 10 minutes Then eligibility for the volunteer is re-evaluated within 1 minute And the volunteer is marked Eligible if the ETA is now at least 10 minutes before start And the system applies the shift-specific buffer over any global default buffer
Mode Preference Application and Fallback Order
Given a volunteer’s mode preference is Bike And the organization’s fallback order is Bike -> Driving -> Walk When bike travel-time is available and the ETA meets the arrival buffer Then the volunteer is marked Eligible using mode = Bike and data_source = LiveTraffic When bike travel-time is unavailable for the route Then the system computes ETA using the next available fallback mode (Driving) And marks the eligibility record with mode = Driving and fallback_from = Bike And applies the arrival buffer to the computed ETA to determine Eligible vs Ineligible
Graceful Degradation to Straight-Line Distance Thresholds
Given the live travel-time API returns errors or timeouts for the origin-destination-mode after 3 retries within 10 seconds Or the system is in an offline state When computing eligibility Then the system computes straight-line (haversine) distance from origin to shift site And compares the distance to the configured max-distance threshold for the volunteer’s mode (e.g., Driving = 10 mi, Walk = 2 mi, Bike = 5 mi in test config) Then the volunteer is marked Eligible if distance <= threshold; otherwise Ineligible And the eligibility record stores data_source = StraightLine, reason = TravelTimeUnavailable, and the distance_km or miles used
Caching and Rate Limiting Controls
Given cache_ttl = 5 minutes and rate_limit = 60 requests per minute are configured for the travel-time service When multiple eligibility checks request the same origin-destination-mode within the TTL Then only the first request calls the travel-time API and subsequent checks use the cached result When incoming requests would exceed 60 travel-time API calls within a rolling 60-second window Then the system throttles additional calls and uses the StraightLine fallback for those checks And the eligibility records for throttled checks include data_source = StraightLine and reason = RateLimited And telemetry counters record cache_hits and throttled_requests for observability
Approximate Location (ZIP Centroid) Support and Privacy
Given a volunteer selects Approximate Location = ZIP 94110 centroid in Location Settings When eligibility is computed Then the origin used is the ZIP centroid coordinates, not device GPS And the system stores and transmits only the centroid coordinates and ZIP identifier in eligibility records and logs And the eligibility origin is labeled as "ZIP 94110 (centroid)" in audit fields And eligibility outcome (Eligible/Ineligible) is determined using the same buffer and mode rules as precise locations
Location Setting Change Recalculates Eligibility
Given a volunteer’s chosen location = Home and current eligibility = Ineligible due to ETA > buffer When the volunteer changes chosen location to Last Check-In in Location Settings Then the system recomputes eligibility immediately (within 10 seconds) using the Last Check-In coordinates, the volunteer’s mode preference, and the shift’s arrival buffer And the eligibility status is updated accordingly (Eligible/Ineligible) And the eligibility record’s audit trail includes prior_origin_type = Home and new_origin_type = LastCheckIn with timestamps
One-Tap Boost/Tighten Controls
"As a captain, I want a single control to widen or narrow the outreach area so that I can quickly adapt to changing needs in the field."
Description

Provides captains with simple mobile controls to temporarily expand or shrink the current outreach radius. Displays a preview of estimated additional or reduced reach (eligible volunteer count) before applying changes. Logs adjustments with timestamps and reasons, and optionally auto-reverts to Smart mode after a configurable duration. Enforces org-defined guardrails for minimum and maximum radius changes and offers accessible, offline-tolerant UI.

Acceptance Criteria
Boost radius with preview and apply
Given a captain is on the Smart Radius panel for an active shift When the captain taps Boost Then the system proposes an increased radius within org guardrails and displays a preview showing current radius vs. proposed radius, current eligible volunteer count vs. proposed count, and the delta And the UI presents Apply and Cancel options without sending outreach When the captain taps Apply Then the new radius is saved and used for eligibility within 5 seconds And the UI confirms the change with a success message and updated radius badge
Tighten radius with preview and apply
Given a captain is on the Smart Radius panel for an active shift When the captain taps Tighten Then the system proposes a decreased radius within org guardrails and displays a preview showing current radius vs. proposed radius, current eligible volunteer count vs. proposed count, and the delta And the UI presents Apply and Cancel options without sending outreach When the captain taps Apply Then the new radius is saved and used for eligibility within 5 seconds And the UI confirms the change with a success message and updated radius badge
Auto-revert to Smart mode after temporary change
Given a boost or tighten is active and Auto-revert is set to a duration D minutes When D minutes elapse without another manual change Then Smart mode is re-enabled and the radius resets to the Smart Radius value And the revert is logged with timestamp, actor=system, and reason="Auto-revert" And the captain is notified via in-app banner within 5 seconds And if the device is offline at the scheduled revert time, the revert executes on reconnect within 10 seconds and is marked as "Deferred" in the log
Adjustment logging with timestamp and reason
Given a captain confirms a boost or tighten via Apply When Apply is tapped Then the app requires the captain to select a reason from a predefined list or choose Other and enter free text up to 140 characters And the event is logged with fields: captain ID, shift/campaign ID, timestamp (UTC), action (boost|tighten), prior radius, new radius, current eligible count, projected eligible count, auto-revert setting, reason And the event is queryable via Activity Log and visible in the Shift Timeline within 5 seconds of Apply
Enforce min/max radius guardrails
Given org-level guardrails define minimum and maximum allowable radius values and/or step sizes When a proposed boost or tighten would exceed these guardrails Then the preview clamps to the nearest allowed value And Apply is disabled if the clamped value equals the current value, with helper text indicating the limit (e.g., "At max radius" or "At min radius") And captains cannot override guardrails from the mobile UI And all attempts beyond guardrails are recorded in analytics with no state change
Accessible, offline-tolerant controls
Given a captain uses assistive technologies or large text When viewing and interacting with Boost/Tighten controls Then controls meet WCAG 2.1 AA: touch targets ≥ 44x44 pt, accessible names/roles/states, logical focus order, and contrast ratio ≥ 4.5:1 And all actions are operable via screen reader and external keyboard Given the device is offline When the captain taps Boost or Tighten Then the preview is computed from the most recent cached density data (timestamped) and marked as an estimate And on Apply, the change is stored locally, marked Pending Sync, and applied to on-device outreach scope And the change syncs to the server within 10 seconds of reconnect and the UI updates to Synced
Scope of radius change on outreach and assignments
Given a boost or tighten has been applied When new outreach messages are triggered for the associated shift/campaign Then eligibility uses the updated radius immediately And scheduled-but-unsent messages are re-evaluated against the updated radius within 60 seconds And existing confirmed assignments are not canceled or modified due to tightening And volunteers outside a tightened radius stop receiving new pings for that shift within 60 seconds
Targeted Outreach Queue
"As an organizer, I want the system to message only the most likely available volunteers so that open slots fill faster with fewer pings."
Description

Builds a real-time recipient list from volunteers within the active radius, filtered by skills, role, availability, and notification preferences. Deduplicates across overlapping shifts, respects per-user message frequency caps, and staggers sends to avoid notification bursts. Integrates with the existing messaging service to trigger pings, reminders, and confirmations, and records outcomes (responses, signups, show-ups) for feedback into Smart Radius tuning.

Acceptance Criteria
Real-Time Queue Build and Update Latency
Given a published shift with an active Smart Radius And volunteers both inside and outside the radius When the shift is created, updated, or the captain adjusts the radius Then the recipient queue is (re)built within 3 seconds of the triggering event And the queue contains only volunteers currently inside the active radius And the queue version increments and the recompute timestamp is recorded And repeated identical events do not change the queue (idempotent)
Filter by Skills, Role, Availability, and Notification Preferences
Given required skills and role for a shift And volunteer profiles with skills, roles, availability (with timezone), and channel preferences When the queue is built Then only volunteers who match all required skills and role are included And only volunteers whose availability covers the shift start time and duration are included And only volunteers opted-in to at least one allowed channel for this shift are included And volunteers with Do Not Disturb or opted-out status are excluded And the final recipient count is displayed
Deduplicate Across Overlapping Shifts
Given two or more overlapping shifts within the same outreach window And volunteers eligible for multiple shifts When the queue is built Then each volunteer appears at most once in the outbound queue And each deduplicated volunteer is assigned to the highest-urgency shift, breaking ties by earliest start time And the assigned_shift_id and dedup_reason are recorded on the queue entry
Enforce Per-User Message Frequency Caps
Given an organization-level cap of 3 outreach messages per rolling 24 hours per user And a volunteer with 2 messages sent in the last 24 hours When two additional outreach messages would be scheduled for the volunteer Then only one message is scheduled and the other is deferred or suppressed to respect the cap And the queue entry records cap_applied=true and exclusion_reason when applicable And no volunteer exceeds the configured cap across all channels
Staggered Sends and Rate Limiting
Given a max_sends_per_minute of 60 and jitter of up to 10 seconds And a recipient queue of 200 volunteers When scheduling message dispatch Then no more than 60 messages are sent in any rolling 60-second window And individual send times include a random jitter of 0–10 seconds And recipients’ quiet hours are respected by shifting sends outside the window
Messaging Service Integration and Delivery Handling
Given a ready recipient queue When dispatch is triggered Then the system calls the messaging API with per-recipient payload (recipient_id, shift_id, channel, template_id, dedupe_key, metadata) And receives and stores message_id per recipient And on transient API failures, retries up to 3 times with exponential backoff And on permanent failures, marks status=failed with error_code and excludes from reminders
Outcome Recording and Feedback Loop
Given sent messages that produce replies, signups, or check-ins When outcome webhooks or app events arrive Then the outcome (responded, confirmed, signed_up, showed_up) is recorded on the queue entry within 2 seconds of receipt And the volunteer’s shift roster and status are updated accordingly And confirmed volunteers are excluded from further reminders for that shift And aggregated outcome metrics are emitted to Smart Radius tuning within 5 minutes
Privacy & Consent Controls
"As a volunteer, I want control over how my location is used so that I feel safe and respected while participating."
Description

Implements explicit opt-in for precise location, with alternatives for approximate location or address-only matching. Provides clear in-app explanations of how location is used, allows volunteers to view/edit stored locations, and honors data retention policies. Minimizes data exposure by processing location with least necessary precision, secures data in transit and at rest, and supports compliance with regional regulations (e.g., GDPR/CCPA). Surfaces privacy settings inline when Smart Radius is first invoked.

Acceptance Criteria
First-Use Inline Privacy Prompt on Smart Radius
Given a volunteer with no prior location consent When Smart Radius is invoked by any feature that requires location Then an inline privacy prompt is shown before any location request or transmission occurs and blocks the workflow until a choice is made Given the inline prompt is displayed When the volunteer reviews it Then it includes: purpose of use, data types, options (Precise, Approximate, Address-only), a link to the privacy policy, and controls (Allow Precise, Allow Approximate, Use Address Only, Not Now) Given no choice has been made Then no location data is read from the device, transmitted, or processed by the server Given the volunteer selects an option Then the system records consent level, timestamp, policy version, org/campaign scope, and locale prior to proceeding
Multi-Level Location Opt-In Options
Given the volunteer selects Precise When Smart Radius runs Then the app requests OS precise location permission and transmits coordinates clamped to 6 decimal places for distance calculations Given the volunteer selects Approximate When Smart Radius runs Then the app does not request OS precise permission and transmits coordinates rounded to 2 decimal places (~1 km), using those values for all matching Given the volunteer selects Address-only When Smart Radius runs Then the app uses the geocoded address centroid rounded to 2 decimal places and makes no GPS requests Given the volunteer changes their consent level When the next Smart Radius calculation occurs Then the new level is applied immediately without requiring app restart
Fallback and Re-Prompt Behavior for Declines
Given the OS denies precise permission while the volunteer selected Precise When Smart Radius runs Then the app offers a one-time inline fallback to Approximate or Address-only and proceeds only if accepted; otherwise the volunteer is excluded from the run Given the volunteer taps Not Now on the privacy prompt Then Smart Radius does not use any location for that volunteer and the prompt is suppressed for 30 days or until the volunteer opens Privacy & Consent settings Given the volunteer declines Precise but allows Approximate or Address-only Then the app does not re-prompt for Precise for 30 days unless the volunteer explicitly changes settings
Volunteer View/Edit of Stored Locations
Given a signed-in volunteer When they open Privacy & Consent > Location Then they can view current consent level, last stored location at its allowed precision, saved addresses, and timestamps Given the volunteer edits or deletes a stored address When they save changes Then the update persists immediately and deleted addresses are removed from active matching within 24 hours Given the volunteer requests deletion of their location records When the request is confirmed Then all location records are purged from primary storage within 24 hours and from backups within 30 days, with an audit entry recorded
Data Minimization and Precision Handling
Given a volunteer has consented to Approximate or Address-only When any location is processed or stored Then no value more precise than 2 decimal places is transmitted or persisted for that volunteer Given a volunteer has consented to Precise When location is stored Then only the most recent coordinate is retained (no history) and is clamped to 6 decimal places; no background tracking occurs Given analytics or application logs are produced When they include references to location operations Then they must not contain raw coordinates or geocodes; only anonymized counts or precision-reduced aggregates are allowed Given Smart Radius inputs are prepared client-side When sending to the server Then values are precision-clamped per consent before transmission
Security of Location Data (Transit and At Rest)
Given any API call includes location data When the request is made Then it uses TLS 1.2+ and requires an authenticated, organization-scoped token; unauthenticated requests are rejected with 401/403 Given location fields are persisted When written to databases or object storage Then they are encrypted at rest with AES-256 or stronger and accessible only to least-privilege service roles; all access is logged Given CI/CD security checks run When a build is produced Then automated tests confirm no location fields appear in plaintext logs and all location endpoints enforce authentication; failing checks block release
Regional Compliance, Retention, and Consent Ledger
Given the volunteer’s region is EEA or UK When the privacy prompt is shown Then default selection is Address-only and Precise requires explicit opt-in with GDPR lawful basis and controller contact displayed Given a data subject access or deletion request is submitted When processed Then the volunteer can access or delete their location data within 30 days; deletions are logged in an auditable ledger Given an organization-level retention policy is configured (default 180 days) When the daily retention job runs Then any location data older than the policy is purged and the purge is recorded with counts Given a volunteer withdraws consent When the change is saved Then all future processing stops immediately and purge of existing location data is initiated per retention policy within 5 minutes
Impact Board & Auto-Tuning Analytics
"As an admin, I want to see and optimize how Smart Radius affects outreach efficiency so that I can improve settings and demonstrate impact."
Description

Captures and visualizes key Smart Radius metrics—notification volume, response rate, fill time, and show-up rate—on the Impact Board. Supports A/B testing of radius heuristics and learns org-specific defaults over time. Provides weekly insights and configurable admin knobs (e.g., aggressiveness of expansion, buffer times). Aggregates and anonymizes data for analysis while preserving individual privacy settings.

Acceptance Criteria
Impact Board KPIs Overview and Freshness
Given I select a date range and optional filters (campaign, shift type, location) on the Impact Board When the board loads or filters are changed Then it displays Notification Volume, Response Rate, Median Fill Time, and Show-up Rate for the selection And each metric shows its value, delta vs the previous equivalent period, and a 7-day sparkline And data latency does not exceed 5 minutes from event ingestion for real-time metrics And filters combine with logical AND and persist across sessions for the user And hovering each metric shows its definition tooltip
A/B Testing of Radius Heuristics
Given an admin with Experiment Manager permission creates an A/B test with two or more radius heuristic variants, target segment, primary metric (Median Fill Time), and a minimum sample size of 200 recipients per variant (configurable) When the experiment is running Then traffic is randomly assigned without overlap and variant assignment is logged And the experiment auto-stops when minimum sample size is met and 95% confidence on the primary metric is reached, or at the configured end date And guardrails pause a variant if its show-up rate drops by ≥10 percentage points or Median Fill Time increases by ≥20% versus control for two consecutive evaluations And the results view reports winner, lift, confidence interval, and per-variant KPIs and allows CSV export
Auto-Learning Org Defaults from Experiments
Given at least 3 completed experiments in the last 90 days with valid results When a variant outperforms the current default on the primary metric with ≥95% confidence for two consecutive weekly windows Then the system updates the org’s Smart Radius default parameters to the winning variant And the change log records timestamp, actor=system, old/new values, evidence links, and provides a one-click rollback for 30 days And subsequent experiments set the updated default as the control unless overridden by an admin
Weekly Insights Generation and Delivery
Given active Smart Radius usage in the prior week When the weekly job runs Then a summary card is posted to the Impact Board and an email is sent to Captains and Admins by Monday 09:00 local time And the report includes top 3 insights with metric impacts, recommended knob changes with predicted effects, and deep links to settings And recipients and delivery time are configurable per org, with a test-send option And if fewer than 50 notifications occurred, the report is skipped and a 'low activity' notice is posted
Admin Knobs Configuration and Preview
Given an Admin opens Smart Radius Settings When they adjust Aggressiveness (0–10), Expansion Step (0.1–5.0 miles or 0.2–8.0 km based on org units), and Buffer Time (5–60 minutes) Then permission checks enforce Admin role and all inputs validate ranges with inline errors And a real-time preview shows estimated reach, expected response rate, and projected fill time deltas based on last 30 days of data And saved changes are applied only to notifications created after save and are captured in an immutable audit log with user, timestamp, and old/new values
Privacy, Aggregation, and Access Controls
Given org privacy settings and user-level analytics preferences When computing analytics and rendering the Impact Board or experiment results Then no PII is displayed; all counts respect k-anonymity with k ≥ 10 or are suppressed/bucketed And users who opted out of analytics are excluded from datasets used for metrics and experiments And analytics exports and emails contain only aggregated fields; no individual-level rows And access to analytics is restricted to roles Captain and Admin; volunteers cannot access org-wide analytics And data deletion requests propagate to analytics stores within 24 hours
Metric Definitions and Calculation Consistency
Given a selected time window When metrics are calculated Then Notification Volume = count of unique recipients notified within the window And Response Rate = unique positive replies divided by unique recipients notified And Median Fill Time = median time from first notification for a shift to assignment of the last required slot And Show-up Rate = number of checked-in assignees divided by number of confirmed assignments And real-time and backfill pipelines match within 1% absolute difference per metric; discrepancies >1% raise an alert

Reliability Waves

Blitz messages go out in prioritized waves using show-up history, proximity, and availability tags, with fairness rotation to prevent burnout. Increases conversion while keeping outreach sustainable and equitable across your roster.

Requirements

Scoring & Ranking Engine
"As an organizer, I want contacts automatically ranked by likelihood and suitability so that my blitz starts with the people most likely to say yes without over-messaging others."
Description

Compute a per-contact priority score that ranks who should be messaged in each wave by combining signals including past show-up/flake history, proximity to the event location, declared availability tags, recent outreach load, and channel consent. Support configurable weighting presets per campaign, explainable scoring breakdowns per contact, and graceful fallbacks for missing data. Recompute scores when roster, event details, or availability changes, and cache top-N results for fast retrieval. Provide a service API consumed by messaging and scheduling layers, sized for rosters up to 50k contacts with p95 top-1k retrieval under 300ms. Store only necessary derived signals and respect privacy settings.

Acceptance Criteria
Score Computation and Ranking Correctness
Given an event E with location and time and a selected campaign weighting preset W And a roster R of up to 50,000 contacts each with show-up/flake history, proximity to E, availability tags for E’s time window, recent outreach load, and channel consent metadata When the engine computes priority scores for messaging channel Ch Then contacts lacking consent for Ch are excluded from eligibility And for each eligible contact, normalized sub-scores in [0,1] are derived for each signal per the spec And the final score S is computed as the weighted sum of sub-scores using W and is in [0,1] And fairness rotation is applied by decreasing S for contacts whose outreach load exceeds the configured threshold And missing signal values apply documented neutral fallbacks without errors (e.g., missing history=0.5, missing proximity=median distance, missing availability=Unknown with zero or reduced weight) And the ranked list is strictly ordered by S descending with deterministic tie-breakers (lower outreach load, closer proximity, then stable by contact_id) And requesting top K returns exactly K contacts if at least K are eligible
Configurable Weighting Presets per Campaign
Given a campaign admin with permissions When the admin creates or updates a weighting preset defining weights for signals that sum to 1.0 (±0.001) Then invalid presets (missing weights, negative values, sum out of tolerance) are rejected with 422 and error details And the active preset can be selected per campaign and per event override And score computations use the active preset immediately after save with propagation p95 ≤ 10s And preset changes are versioned and audit-logged with user, timestamp, and diff And the system supports at least 10 named presets per tenant
Explainable Per-Contact Score Breakdown
Given a computed score for contact C for event E When the client requests the breakdown Then the API returns for each signal: signal_name, raw_value, normalized_value, weight, contribution, fallback_applied (boolean), and notes And the sum of contributions equals the final score within ±0.001 And the response includes overall eligibility flags (e.g., consent_excluded) and the applied tie-breaker order And no prohibited fields (PII beyond contact_id) are present And the response is generated in p95 ≤ 200ms for single-contact breakdowns
Reactive Recompute on Data Changes
Given scores cached for event E When any of the following change: event time or location, a contact’s availability tags for E’s time window, show-up/flake history, proximity inputs, outreach load counters, or consent status Then only impacted contacts are re-scored incrementally And caches for affected top-N results are invalidated and refreshed And end-to-end propagation to reflected rankings is p95 ≤ 10s, p99 ≤ 60s And updates are idempotent and result in monotonic version increments for ranking snapshots
Top-N Caching and Retrieval Performance SLOs
Given a roster size up to 50,000 and concurrent 20 QPS queries for top-1,000 When requesting top-N for N ∈ {50, 100, 250, 1000} Then p95 latency ≤ 300ms and p99 latency ≤ 600ms measured at service boundary And cache hit rate for repeated identical queries within TTL ≥ 90% And result ordering is stable across identical inputs and repeated calls And memory usage of caches stays within configured limits and no unbounded growth occurs
Privacy, Consent, and Data Minimization Compliance
Given tenant privacy settings and per-contact consent records When computing, storing, or returning scores Then only necessary derived signals listed in the data inventory are stored; raw PII (e.g., exact addresses) is not persisted by the engine And contacts with revoked consent for Ch are excluded from rankings for Ch within p95 ≤ 10s And any previously stored derived signals for a revoked contact are deleted or anonymized within 24 hours And access to scoring endpoints requires authorization; all access is audit-logged
Service API Contract and Integration
Given authenticated clients from messaging and scheduling layers When invoking the API endpoints: - GET /rankings?event_id=E&channel=Ch&k=K - GET /breakdown?event_id=E&contact_id=C - POST /presets (admin only) Then requests and responses conform to the published JSON schema with field types and required attributes validated And rate limiting, pagination (where applicable), idempotency, and error mapping (4xx for client errors, 5xx for server) are enforced And API versioning via Accept-Version header is supported with backward-compatible changes And end-to-end integration tests from messaging/scheduling can retrieve top-K and send without additional sorting
Wave Orchestration & Throttling
"As a campaign lead, I want blitz messages to go out in controlled waves with safeguards so that we fill shifts quickly without overwhelming volunteers or breaking rate limits."
Description

Define and execute prioritized message waves with configurable batch sizes, send cadence, and channel mix (e.g., SMS, email). Enforce rate limits per channel and provider, and support pause/stop conditions such as targets filled, high opt-out rate, or low reply quality. Allow time offsets between waves, automatic progression until goals are met, and retries with backoff on transient failures. Emit detailed delivery and response logs for monitoring and audit. Integrate with existing messaging providers and fall back seamlessly on provider failures.

Acceptance Criteria
Prioritized wave execution with batch, cadence, and channel mix
Given a campaign wave configured with priority weights {show_up_history:0.5, proximity:0.3, availability_match:0.2}, batch_size=200, send_cadence=30 messages/minute, channel_mix={SMS:60%, Email:40%} When the wave is started Then recipients are ordered by descending weighted score with ties stable-sorted by signup_time ascending And messages are dispatched at a rate ≤30 messages/minute averaged over any rolling 60-second window And no batch exceeds 200 recipients And across the first 1000 sends the channel distribution is SMS 60%±3% and Email 40%±3%
Per-channel and per-provider rate limiting
Given global channel limits {SMS:180/min, Email:600/min} and provider limits {Twilio_SMS:60/min, Nexmo_SMS:120/min, SendGrid_Email:400/min} When multiple waves target all channels concurrently Then observed send rates never exceed each provider limit over any rolling 60-second window And observed send rates never exceed each channel limit over any rolling 60-second window And excess messages are queued and drained FIFO when capacity becomes available And no message is dropped due to rate limiting
Auto pause/stop on fulfillment, opt-out spike, or low reply quality
Given a running wave with a campaign goal of 100 confirmed shifts When confirmed shifts reach 100 Then the wave stops within 60 seconds and no further messages are sent And a goal_met event is recorded and an alert is emitted Given a running wave with ≥200 deliveries in the last 15 minutes and opt_out_threshold=5% When opt-out rate over that window reaches ≥5% Then the wave pauses automatically and requires explicit resume Given low_reply_quality_threshold=0.4 and ≥100 replies in the last 30 minutes When average reply quality score over that window falls below 0.4 Then the wave pauses automatically and an alert is emitted
Time offsets and automatic progression across waves
Given three waves configured with offsets [0m, +15m, +45m] and a campaign goal of 500 deliveries When wave 1 completes and 15 minutes elapse Then wave 2 starts automatically without operator action And after an additional 30 minutes elapse, wave 3 starts automatically And waves stop progressing when 500 deliveries are achieved or a stop condition is triggered And if the system is paused during an offset window, the next wave starts within 60 seconds of resume if its offset has elapsed
Retry with exponential backoff on transient failures
Given transient error responses (HTTP 429/5xx or network timeouts) from a provider When a send attempt fails due to a transient error Then the system retries with exponential backoff starting at 30s with jitter ±20%, doubling each attempt up to 5 attempts And non-retriable errors (HTTP 4xx except 429) are not retried And successful delivery on retry is logged with retry_count and final status delivered And exhausted retries result in final status failed with the last error_code recorded
Delivery and response logging for monitoring and audit
Given messages are sent as part of a wave When viewing logs via API or CSV export Then each record includes: message_id, wave_id, campaign_id, recipient_id, channel, provider, template_id, queued_at, sent_at, delivered_at (nullable), status, error_code (nullable), retry_count, rate_limit_bucket, response_thread_id (nullable), reply_quality_score (nullable), opt_out_flag And timestamps are UTC ISO-8601 with millisecond precision And logs are filterable by wave_id, channel, provider, status, and date_range And records are retained for 365 days and access-controlled by campaign role
Provider failover with deduplication and limits preserved
Given a primary SMS provider outage lasting ≥60 seconds with 5xx error rate ≥80% When sending during the outage Then subsequent SMS sends are routed to the configured secondary provider within 60 seconds of detecting the outage And no recipient receives duplicate content for the same message_id And channel and provider rate limits continue to be enforced on the secondary provider And the system fails back after 5 consecutive minutes of primary provider health with ≤1% 5xx And a failover event with start and end timestamps is logged
Fairness Rotation & Burnout Guardrails
"As a volunteer coordinator, I want built-in fairness and cooldown rules so that outreach stays sustainable and no one gets burned out or over-solicited."
Description

Maintain a per-volunteer solicitation ledger tracking last-contacted, last-served, and cumulative asks over rolling windows. Enforce fairness rules such as frequency caps, cooldown periods after a shift, and equitable rotation within priority tiers to prevent repeatedly pinging the same people. Apply light randomization within tiers to distribute opportunities while still honoring priority. Provide fairness metrics and alerts (e.g., concentration of asks) and require admin justification for overrides, with changes logged.

Acceptance Criteria
Rolling Frequency Cap Enforcement (7-Day Window)
Given frequency_cap_per_7d = 2 and volunteer A has asks_count_7d = 2 When a new blitz wave is generated Then volunteer A is excluded from the candidate set and exclusion_reason = "frequency_cap" is recorded on the wave and in A's ledger audit Given frequency_cap_per_7d = 2 and volunteer B has one ask logged at t0 = now - 7d - 1m When eligibility is recomputed at wave generation time Then B's asks_count_7d recalculates to 0 and B becomes eligible for inclusion Given the candidate export is requested after wave generation When reviewing A and B Then A appears with exclusion_reason = "frequency_cap" and B appears with eligibility_reason includes "cap_reset"
Post-Shift Cooldown Guardrail
Given cooldown_after_shift_hours = 24 and volunteer C completed a shift ending 3 hours ago When a blitz wave is generated Then volunteer C is excluded with exclusion_reason = "cooldown_post_shift" and cooldown_expires_at = shift_end + 24h is displayed Given volunteer D completed a shift ending 25 hours ago When a blitz wave is generated Then D is eligible with no cooldown-related exclusion reason Given an admin attempts to manually add C to the wave without override When saving the wave Then the save is blocked with an error referencing "cooldown_post_shift"
Equitable Rotation Within Priority Tier
Given tier = "High Priority" with eligible volunteers {E,F,G} and rotation_mode = enabled When Wave 1 selects 2 volunteers Then the selected set is any 2 of {E,F,G} and rotation_state records the contacted volunteers Given Wave 2 is generated before the rotation resets When selecting from the same tier Then only the remaining uncontacted volunteer is selected (subject to eligibility filters), and no volunteer contacted in Wave 1 is reselected Given one volunteer becomes ineligible between waves When generating the next wave Then rotation_state skips ineligible volunteers without resetting, and the cycle completes when all currently eligible volunteers have been contacted once
Light Randomization With Priority Preservation
Given a priority-ordered list L within a tier and randomization_window_k = 3 and seed = "2025-08-09" When generating a wave Then each volunteer's final position differs from their base priority by no more than ±3 slots and the output order is reproducible with the same seed Given the same inputs but seed = "alt" When generating a wave Then the resulting order differs from the order produced with seed = "2025-08-09" in at least one position Given volunteers at higher base priority exist When randomization is applied Then no volunteer may leapfrog beyond randomization_window_k positions over any higher-priority volunteer
Solicitation Ledger Update and Idempotency
Given a wave is sent to volunteers {H,I} with idempotency_key = W123 When messages are dispatched Then ledger.last_contacted_at is updated for H and I, asks_count_7d and asks_count_30d are incremented by 1, and an audit entry is written with wave_id=W123 Given the dispatch is retried with the same idempotency_key = W123 When processing retries Then no duplicate increments occur and the audit shows a single logical send per volunteer Given volunteer H completes a shift at 17:00 local time When the shift is marked complete Then ledger.last_served_at = 17:00 local time and all time-based calculations use the organization's time zone setting
Admin Override With Justification and Audit Log
Given an admin with role = Organizer attempts to include a volunteer excluded by frequency cap When selecting "Override" on the exclusion Then the system requires a justification >= 10 characters and blocks save until provided Given the override is confirmed When the wave is saved Then an audit record is created capturing admin_id, timestamp, overridden_rules ["frequency_cap"], justification text, and before/after eligibility state, and the volunteer is included with inclusion_reason = "admin_override" Given a user without override permission attempts the same action When clicking "Override" Then the action is denied with a permission error and no changes are saved
Fairness Metrics and Ask-Concentration Alerts
Given a 30-day lookback window and alert_threshold_top10_share = 0.35 When nightly fairness metrics are computed Then the system calculates share_of_asks_top10pct and, if > 0.35, creates an ask_concentration_alert with metric values and affected cohort details Given an alert is active When a user opens the Impact Board Then an in-app banner summarizes the metric, links to a fairness report, and the alert status is visible until the metric remains below threshold for 48 consecutive hours Given alerts are acknowledged When metrics fall below threshold for 48 hours Then the alert auto-resolves and resolution is logged with timestamp and metric snapshot
Availability & Proximity Data Integration
"As a field organizer, I want availability and distance automatically factored into targeting so that I only ping people who can realistically make the shift."
Description

Ingest and normalize availability tags from profiles and recent shift activity, align them with event slot times, and compute match windows by time zone. Derive proximity using stored home location or last known check-in and an event’s location with adjustable radius and transportation mode assumptions. Support manual overrides, virtual events (ignore distance), and fallback logic when data is incomplete. Keep data fresh via triggers on profile updates and event changes, and apply retention limits aligned with privacy policy.

Acceptance Criteria
Normalize Availability Tags from Profiles and Shifts
Given a member profile contains availability tags with mixed casing, synonyms, and duplicates And the member has recent shift activity within the configured lookback window When the ingestion and normalization job runs Then tags are mapped to canonical availability blocks per org configuration And duplicates/synonyms are collapsed to a single canonical value with stable IDs And canonical availability entries record source=profile or source=shift with timestamps And invalid/unmapped tags are ignored and logged without failing the job And repeated runs with the same inputs produce identical outputs (idempotent)
Time Zone Match Window Computation for Event Slots
Given an event slot has a start and end time in the event’s IANA time zone And a member’s availability is stored relative to the member’s IANA time zone When computing availability match windows for that member and event Then member availability is converted to the event time zone and intersected with the slot And resulting match windows are stored as UTC timestamps with the source time zone recorded And daylight saving time transitions are correctly handled at boundaries And partial overlaps meeting the configured minimum overlap threshold are marked as matches
Proximity Calculation with Adjustable Radius and Transport Mode
Given a member has a stored home location (latitude/longitude) and an optional last check-in with a timestamp And an event has a physical location (latitude/longitude) And a proximity radius and transportation mode are provided (or defaults are configured) When computing proximity Then the location source is selected as last check-in if it is more recent than the home location update, otherwise home location And geodesic distance between the selected member location and the event location is computed And within_radius is true if distance is less than or equal to the configured radius for the selected mode And the output includes distance_km, mode_used, radius_used, and location_source And changing the radius or mode yields updated within_radius results accordingly
Manual Overrides Precedence and Auditing
Given an authorized user sets a manual override for availability and/or proximity for a member and/or specific event When match calculation is executed Then override values are applied in place of derived values for the targeted scope (global or event-specific) And overrides include actor, timestamp, scope, and optional expiration And expired overrides are ignored automatically without error And an audit record is persisted showing the override application during the calculation
Virtual Events Distance Ignored
Given an event is marked as virtual or has no physical location When computing proximity for any member Then distance calculations are skipped and within_radius is set to true And availability matching is still computed against the event’s declared time zone And manual overrides can still force exclusion or inclusion as configured
Fallback Logic on Incomplete Data
Given a member is missing availability tags and has no qualifying recent shifts within the lookback window Or the member lacks both a home location and a last check-in When the system attempts to compute availability matches and proximity Then derived fields are set to unknown with explicit reason codes (e.g., missing_availability, missing_location) And no unhandled errors are thrown and the job completes successfully And unknown values do not assert positive matches or exclusions by default and are emitted for downstream handling
Data Freshness Triggers and Retention Limits
Given a profile’s availability tags or home location are updated, or an event’s time, location, or virtual flag changes When the change is committed Then dependent availability matches and proximity values are recomputed within 60 seconds for affected member–event pairs And recomputations are idempotent and logged with correlation to the triggering change And data older than the configured retention window is purged or anonymized in accordance with the privacy policy And purge jobs produce a report of records affected without removing active overrides
Adaptive Wave Tuning via Live Responses
"As a captain running a blitz, I want later waves to adapt based on what’s working so that I hit my goals faster with fewer messages sent."
Description

Continuously ingest replies and signups to update fill counts and dynamically re-rank the remaining audience before each subsequent wave. Auto-stop further waves when slots are filled; auto-expand to next-best candidates if conversion underperforms. Support message variant testing and automatically weight better-performing variants. Surface real-time metrics (reach, reply rate, conversion, time-to-fill) and write results to the Impact Board and analytics exports/webhooks.

Acceptance Criteria
Real-time Response Ingestion Updates Fill Counts
Given an active blitz with open shift slots and enabled inbound channels (SMS, email, web form) When a contact replies with a positive RSVP (e.g., YES) or completes a signup tied to a specific shift Then the shift’s filled_count increments exactly once for that contact within 5 seconds And the contact is assigned to the shift with a confirmed status and receives an auto-confirmation message And duplicate acceptances across channels for the same contact+shift are deduplicated (idempotency enforced by event_id) And invalid or ambiguous replies do not increment filled_count and trigger a clarification prompt And an audit log entry is recorded with timestamp, channel, contact_id, shift_id, and outcome
Pre-Wave Dynamic Re-Ranking With Fairness Rotation
Given the next outreach wave is being prepared and a candidate audience exists When the system computes the send list Then candidates are scored using show-up history, proximity, and availability tag match And fairness rotation excludes contacts who exceed contact frequency caps (e.g., >2 blitzes in past 14 days or targeted for the same shift type in past 48 hours) And opted-out or suppressed contacts are excluded And the ranked snapshot is persisted with a version_id and criteria hash And preparation latency is ≤ 2 seconds per 10,000 candidates And the UI displays the next-wave candidate count and the applied fairness caps
Auto-Stop Waves On Fill Completion
Given a blitz with target slots per shift When filled_count for a shift reaches the target and all in-flight confirmations are processed Then all unsent messages in pending waves for that shift are canceled within 5 seconds And no additional messages are sent for that shift after cancellation (0 sends post-stop) And the shift status updates to Filled and a completion entry is posted to the Impact Board And final metrics for the shift are frozen and visible in the dashboard and exports
Auto-Expand Audience When Conversion Underperforms
Given conversion targets and minimum sample size thresholds are configured When, after a completed wave, observed conversion is below the configured threshold and sends ≥ the configured minimum sample size and max outreach caps are not exceeded Then the system expands outreach by adding the next-best ranked candidates for the following wave And all suppression and fairness rotation rules still apply to the expansion And the expansion size and rationale (gap-to-fill, observed conversion) are logged and shown in the UI And expansion occurs only if time-to-shift-start exceeds the configured safety window And metrics update to reflect expansion scope and expected fill
Variant Testing and Adaptive Weighting
Given 2–5 message variants are configured for a blitz When Wave 1 is sent Then initial allocation is randomized approximately evenly across variants (±5%) And reply rate and conversion are tracked per variant When each variant has at least the configured minimum sample size (e.g., ≥100 sends per variant) and sufficient responses Then send weights are automatically adjusted to favor higher-converting variants, with guardrails (e.g., best variant ≥50% of remaining sends; any variant ≥10% until ≥1000 total sends) And any variant underperforming at <50% of the top variant after ≥500 sends is paused And all weighting/pausing decisions are recorded with timestamp, variant_id, and metrics
Real-time Metrics, Impact Board, and Exports/Webhooks
Given a live blitz with ongoing waves When viewing the dashboard Then reach, reply rate, conversion, and time-to-fill display with p95 data freshness ≤10 seconds And metric definitions match product documentation When the blitz fills or ends Then the Impact Board updates with final totals within 15 seconds And analytics exports include per-wave and per-variant rows with timestamps, counts, rates, and fill milestones And webhooks emit idempotent events (with unique event_id) within 10 seconds and retry on failure with exponential backoff up to 3 attempts
Admin Console for Wave Configuration
"As an admin, I want an easy setup and preview experience for Reliability Waves so that I can configure campaigns confidently and avoid mis-targeting."
Description

Provide a guided UI to create, preview, and manage wave campaigns: set scoring weights, fairness caps, quiet hours, message templates per channel, and stop conditions. Show audience estimates and sample contact lists with reasons for inclusion/exclusion. Offer a dry-run simulator that forecasts expected fills by wave and highlights potential risks (e.g., thin availability). Include role-based access, drafts, versioned templates, and change history with one-click rollback.

Acceptance Criteria
Create Wave Campaign with Scoring and Fairness
- Given I am on the Wave Campaign wizard, When I enter a campaign name and select a target shift, Then the Next button becomes enabled. - Given the scoring weights form, When I set weights that sum to any value, Then the UI accepts them and normalizes internally with a visible preview of relative influence. - Given fairness caps, When I set max touches = 2 per 7 days and min cooldown = 3 days, Then the preview indicates which contacts would be deferred by fairness rules. - Given required fields are incomplete, When I attempt to proceed to Preview or Save, Then I see inline validation messages and cannot advance. - Given I click Save Draft, When validation passes for the current step, Then the draft saves with a timestamp and appears in the Drafts list. - Given a saved draft, When I reopen it, Then all previously entered configurations are restored exactly. - Given the final Review step, When I click Publish, Then the campaign is created with status Scheduled and is immutable except via versioned edits.
Configure Quiet Hours and Channel Templates
- Given Quiet Hours settings, When I set quiet hours 21:00–08:00 by recipient local timezone, Then the preview excludes sends scheduled within that window. - Given per-channel templates (SMS and Email), When I insert merge fields {{first_name}} and {{shift_time}}, Then the template validator confirms all fields resolve from the selected campaign context. - Given an invalid merge field is used, When I attempt to save, Then I receive a blocking error identifying the exact missing field. - Given SMS character counter, When my SMS exceeds 160 chars, Then I see segment count and cost impact before save. - Given channel fallbacks, When a contact lacks an SMS opt-in but has email, Then the preview routes the message via Email and records the reason. - Given quiet hours are enabled, When I schedule an immediate send during quiet hours, Then the system delays per recipient until the window ends and shows the adjusted time in preview.
Audience Estimate and Inclusion/Exclusion Reasons
- Given a configured campaign, When I open the Audience tab, Then I see total estimated reachable contacts by wave and by channel. - Given I click View Sample, When the sample loads, Then I see at least 50 contacts with per-contact reasons for inclusion (e.g., proximity score, availability match) or exclusion (e.g., fairness cap, quiet hours, opt-out). - Given filters are changed (weights, caps, tags), When I click Refresh Estimate, Then counts update within 5 seconds for audiences up to 50k. - Given an exclusion reason, When I hover or tap the reason, Then I see the rule that caused it and the data that matched the rule. - Given the estimate, When I export the sample, Then a CSV downloads containing contact ids, channel, wave assignment, and reason codes.
Dry-Run Simulator Forecasting and Risk Highlighting
- Given a configured campaign with historical data, When I run Dry-Run, Then the simulator returns expected fills by wave with 80% confidence intervals and median within 10 seconds for audiences up to 50k. - Given thin availability for the target shift, When expected fills < 70% of target after all waves, Then the simulator flags Risk: Thin Availability with a descriptive banner. - Given I adjust scoring weights or fairness caps, When I re-run the simulator, Then results update and the change log records the parameter deltas. - Given conversion assumptions, When I open Assumptions, Then I can view and override default conversion rates per wave and channel, and the forecast recalculates accordingly. - Given the simulator output, When I click Export, Then a PDF/CSV report downloads including per-wave audience, expected fills, assumptions, and identified risks.
Stop Conditions and Auto-Halt Behavior
- Given stop conditions are set to Stop at 25 fills or Max 3 waves, When expected fills reach 25 in simulation, Then the run plan shows waves beyond the stop as Not Needed. - Given a live send, When actual confirmed fills reach the stop threshold, Then subsequent waves are automatically canceled and no additional messages are sent. - Given a canceled wave due to stop, When I view the campaign status, Then I see Stopped: Target Reached with timestamp and counts. - Given role permissions, When a user without Admin role attempts to override stop and resume, Then the action is blocked with an authorization error. - Given an override by an Admin, When resume is confirmed, Then an audit entry records who, when, and new stop conditions.
Role-Based Access and Permissions
- Given RBAC roles (Admin, Editor, Viewer), When a Viewer opens the console, Then they can view campaigns and history but cannot edit, publish, or rollback. - Given an Editor, When they create or edit a draft, Then they can save drafts but cannot publish or rollback without Admin. - Given an Admin, When they publish or rollback a campaign, Then the action succeeds and is attributed to their user identity in history. - Given API enforcement, When a user attempts a restricted action via API, Then the server responds 403 and no state changes occur. - Given audit trails, When any permission-denied event occurs, Then it is logged with user id, action, target, and timestamp.
Drafts, Versioned Templates, Change History, and Rollback
- Given a published campaign, When I modify templates or rules and click Publish Changes, Then a new version is created with incremental version number and diff summary. - Given change history, When I open History, Then I see a chronological list with user, timestamp, version, and change summary, and can filter by change type. - Given a selected past version, When I click One-Click Rollback and confirm, Then the system restores all templates and settings from that version and records a new version labeled Rollback. - Given a scheduled campaign with future sends, When I rollback, Then the preview and simulator reflect the restored configuration for any unsent waves. - Given data integrity, When I rollback and then forward-compare, Then no differences exist between the current configuration and the chosen historical version.
Consent, Compliance, and Quiet Hours
"As an organizer, I want compliance and quiet-hour rules enforced automatically so that our outreach remains legal and respectful without manual checks."
Description

Enforce per-channel opt-in, automatic handling of opt-out keywords, local-time quiet hours, and per-user frequency caps across campaigns. Record consent provenance and provide exportable audit logs. Validate campaigns pre-send for compliance risks and block sends that would violate rules. Support language preferences for templates and ensure compliant footer/opt-out language is included where required.

Acceptance Criteria
Per-Channel Opt-In Gate at Send Time
Given a campaign targets SMS and Email with a mixed-consent audience When the user initiates the send Then only recipients with active opt-in for the specific channel are queued per channel And recipients lacking opt-in for a channel are excluded from that channel with a compliance error list available for export And no message is delivered on any channel for which the recipient lacks consent And the compliance decision (allowed/blocked) is recorded per recipient and channel in the audit log with timestamps
Opt-Out Keyword Handling and Immediate Suppression
Given a recipient sends an SMS reply containing an opt-out keyword (e.g., STOP, UNSUBSCRIBE) in a supported language When the inbound message is received Then the recipient's SMS consent is set to Opted-Out within 60 seconds And a confirmation SMS is sent in the recipient's language acknowledging the opt-out with help instructions And all future SMS sends to that recipient are blocked across all campaigns and automations And an audit log entry captures timestamp, channel, keyword detected, message ID, campaign (if applicable), and actor=system And if the recipient later sends a recognized re-opt-in keyword (e.g., START), consent is restored to Opted-In with provenance recorded
Local-Time Quiet Hours Enforcement
Given organization quiet hours are configured (e.g., 21:00–08:00) and each recipient has a resolvable local time zone or falls back to org default When a campaign or automation attempts to send during a recipient's quiet hours Then the send to that recipient is deferred until the next allowed window while preserving wave priority And recipients whose local time is outside quiet hours receive messages immediately And no recipient receives messages during their quiet hours And all defer/block decisions are logged per recipient with resolved time zone, planned send time, and release time
Per-User Frequency Caps Across Campaigns
Given organization policy defines caps (e.g., SMS ≤ 3 per 24h, Email ≤ 5 per 7d, Calls ≤ 2 per 7d) using rolling windows When a campaign or automation would cause an individual to exceed a channel cap Then that channel send is excluded for the individual with remaining cooldown time reported And the individual may still receive the campaign on other channels where caps are not exceeded And caps are enforced across all campaigns, waves, blasts, and automations And cap counters update in real time and reset according to rolling-window logic And exclusions are summarized by channel with counts and downloadable detail
Consent Provenance Capture and Exportable Audit Logs
Given a recipient provides consent via any channel (web form, SMS keyword, staff entry, import) When consent is stored or updated Then provenance records include timestamp (UTC), channel, source method, actor (user/system), language, consent text/template version ID, and relevant metadata (IP/user agent for web) And an immutable audit event is written with before/after consent state And administrators can export audit logs filtered by date range, channel, campaign, and contact to CSV including all provenance fields
Pre-Send Compliance Validator Blocks Non-Compliant Campaigns
Given a campaign draft has content and a target audience defined When the compliance validator runs or the user attempts to send Then the system blocks the send if violations exist, including missing opt-out footer, recipients without required opt-in, recipients exceeding frequency caps, or quiet-hours conflicts for immediate sends And the validator displays violation categories with counts and up to 100 sample records per category, plus downloadable full lists And once all violations are resolved, the validator returns Pass and allows sending And a validation report is saved to the campaign record with timestamp and validator version
Language Preference and Required Footer Inclusion
Given recipients have language preferences and channel-specific compliance requirements apply When messages are rendered for delivery Then each recipient receives the localized template matching their language preference And required opt-out instructions/footers are included per channel and locale (e.g., SMS: Reply STOP to stop; Email: one-click unsubscribe link) And if a locale template is missing, the system falls back to the org default language and flags a validator warning And the final rendered content, resolved language, and footer-included flag are recorded in the audit log per message

Claim Hold

When a volunteer taps the link, the slot is reserved for a short window and confirmed with one more tap; unconfirmed holds auto-release. Prevents double-assignments and keeps rosters accurate in fast-moving moments.

Requirements

Tokenized Claim Links & Identity Verification
"As a volunteer, I want a secure, personalized link that takes me straight to my slot so that I can quickly claim without logging in or re-entering my info."
Description

Generate per-slot, time-bounded, single-use claim URLs embedded in SMS, email, and push that securely identify the volunteer via signed tokens. Deep-link into the mobile app (with web fallback), pre-fill the volunteer profile, enforce eligibility (role, training, conflict checks), and prevent link re-use or sharing. Include token expiry, rate limiting, and idempotency to handle retries. Log every attempt for auditability and tie records to the Contact profile to streamline the one-tap confirmation flow.

Acceptance Criteria
Single-Use Hold and Auto-Release
Given the organization hold window is set to 10 minutes and the slot is open When a volunteer opens a valid claim link for that slot Then the system places a hold on the slot for that volunteer for 10 minutes And other volunteers see the slot as unavailable during the hold When the volunteer taps Confirm within the hold window Then exactly one assignment is created and the hold is cleared When the hold window expires without confirmation Then the hold auto-releases and the slot returns to open status And no assignment is created
Signed Token Identity & Profile Prefill
Given a claim URL contains a valid, non-expired signed token for Contact X and Slot Y When the link is opened on a device Then the backend validates the token signature and scope (Contact X, Slot Y, channel) And the app or web claim screen opens for Slot Y And the volunteer profile fields (name, email, phone) are prefilled from Contact X And no login is required to view the prefilled claim screen
Eligibility Enforcement Gate
Given Contact X does not meet one or more eligibility rules (role, training, conflict checks) for Slot Y When Contact X attempts to confirm the claim Then the confirmation is blocked And the UI displays each failing rule with a human-readable reason And no assignment is created and the hold (if active) remains until it expires When all failing conditions are resolved and Contact X re-attempts within the hold window Then the confirmation succeeds
Reuse/Sharing Prevention & Messaging
Given a claim link token for Contact X and Slot Y has been consumed by a successful confirmation When any user attempts to use the same link again Then the request is rejected as already used and the UI shows "Link already used" with a CTA to request a new link And no duplicate assignment or new hold is created When the link is opened by a session associated with a different Contact Z Then the system denies confirmation and shows "This link isn’t for you" messaging with safe navigation options And all denied attempts are logged
Expiry, Rate Limiting, and Idempotent Retries
Given the token TTL is configured to 24 hours and the rate limit is 3 claim attempts per minute per Contact When a link is opened after the token TTL Then the system responds as expired and no hold or assignment is created When multiple confirm requests for the same token occur within 5 seconds due to retries Then exactly one assignment is created and subsequent requests return the same confirmation result idempotently When attempts exceed the configured rate limit within the window Then further requests are rejected with a 429 response and backoff messaging
Audit Logging Linked to Contact
Given audit logging is enabled When any claim lifecycle event occurs (open, hold, confirm, reject, expire, rate-limit) Then an audit record is written within 5 seconds including Contact ID, Slot ID, token ID, channel, source IP/UA, timestamp, outcome, and reason And audit records are visible under the Contact profile Activity and retrievable via admin export/API And audit records are immutable and include a correlation ID for support
Cross-Channel Deep Link & Web Fallback
Given claim links are delivered via SMS, email, and push notification When the link is tapped on iOS or Android with the GiveCrew app installed Then the app opens directly to the claim screen via deep link in 2 seconds or less When the app is not installed Then the link opens a mobile web claim page preserving token and slot context And UTM/channel parameters are preserved for analytics attribution across both app and web
Atomic Slot Reservation & Concurrency Control
"As an organizer, I want the system to prevent two people from taking the same slot so that rosters stay accurate during rush signups."
Description

Implement backend locking that atomically places a short-lived hold on a slot the moment a claim link is tapped, preventing double-assignment under high concurrency. Use transactional operations and idempotency keys to handle rapid taps and retries across devices. Provide deterministic conflict handling and user-facing messages when a slot is already held or filled. Ensure horizontal scalability and low latency so holds are consistent across regions and clients.

Acceptance Criteria
Single-Tap Atomic Hold Under High Concurrency
Given a slot is OPEN and 2–100 claim requests arrive within 0–100 ms When requests are processed Then exactly one hold is created with a unique hold_id and expires_at = now + 120s, and all other requests receive 409 SLOT_HELD with remaining_ttl ≥ 115s, and no two holds exist for the same slot at any time And P95 hold request end-to-end latency ≤ 150 ms; P99 ≤ 300 ms And cross-region requests do not create split-brain holds (duplicate holds rate ≤ 1e-7 over a 1-hour soak)
Idempotent Rapid Re-Taps and Cross-Device Retries
Given a client sends repeated hold requests with the same idempotency_key within the hold TTL When these requests are processed Then the same hold_id and expires_at are returned with HTTP 200 and no additional holds are created And network retries or two devices using the same idempotency_key receive identical responses and side effects occur once And if a different idempotency_key is used by the same user while a hold exists, the server returns 409 DUPLICATE_ACTION with the existing hold summary; no new hold is created
Hold Expiration and Auto-Release
Given a hold is not confirmed within 120s When expires_at is reached Then the slot becomes AVAILABLE within ≤ 1s and the hold is marked EXPIRED; subsequent confirm attempts receive 409 HOLD_EXPIRED And a background sweeper reaps expired holds at ≤ 5s intervals with zero residual locks And roster/slot state is consistent across all regions within 1s of expiry
Deterministic Conflict Messaging
Given a slot is already held by another user When a new claim attempt arrives Then respond with 409 SLOT_HELD including remaining_ttl (integer seconds) and next-step hints; no ambiguous 2xx/5xx responses Given a slot is already filled When a claim attempt arrives Then respond with 410 SLOT_FILLED including assignee_display and alternative slot links Given simultaneous confirm attempts for the same slot When processed Then one returns 200 CONFIRMED and the others return 409 HOLD_CONFLICT with a consistent error schema version
Confirmation Commit and Double-Assignment Prevention
Given a valid hold exists for user U When U taps Confirm within TTL Then assignment is committed atomically and the hold is deleted in the same transaction; the slot has exactly one assignee And retrying Confirm with the same idempotency_key returns the same final state without duplicate side effects Given two different users confirm concurrently for the same slot When processed Then a unique constraint prevents double-assign; one succeeds and the other receives 409 SLOT_FILLED And P95 confirm latency ≤ 150 ms; P99 ≤ 300 ms
Horizontal Scale and Consistency SLOs
Given 500 RPS mixed hold/confirm traffic across 3 regions with ~150 ms inter-region RTT When load is applied for 30 minutes Then hold/confirm error rate ≤ 0.1%, duplicate holds per slot = 0, duplicate assignments per slot = 0 And region failover drains one region without orphaning holds; in-flight holds are honored and visible globally within ≤ 2s And with clock skew of ±200 ms, TTL and conflict logic do not cause premature expiry or extensions beyond TTL + 2s
Auditability, Metrics, and Alerting
Given any hold or confirm event When it completes Then an immutable audit log records slot_id, actor_id (or anonymous token), idempotency_key, request_id, region, outcome, and timestamps; 99.9% of events are queryable within 5 minutes And health alerts trigger within 60s upon detection of a duplicate hold/assignment attempt or SLO breach And dashboards expose P50/P95/P99 latency for hold/confirm, active holds, expiry rate, conflicts/minute, and remain within monitoring cardinality budgets
Hold Countdown & Auto-Release
"As a volunteer, I want a clear countdown and automatic release if I don't confirm so that I don't block others when I'm unsure or distracted."
Description

Display a clear, real-time countdown timer for the hold window and automatically release the slot if not confirmed before expiry. Make the hold duration configurable at org, campaign, and shift levels with sensible defaults. Enforce expiry server-side, recover timers after app refresh, and reconcile if devices go offline. On release, immediately reopen the slot to the next candidate or waitlist according to assignment rules and log the outcome.

Acceptance Criteria
Default Countdown Timer Renders and Updates in Real Time
Given a volunteer taps a claim link for an available slot And no custom hold duration is set at the shift, campaign, or org level When the hold is created Then the UI displays a countdown initialized to the system default hold duration And the countdown updates at 1-second intervals without pausing while the screen is active And the remaining time displayed matches the server expiry within ±1 second And the Confirm action remains enabled while remaining time > 0 And the Confirm action is disabled and visually indicates expiry at 0 seconds
Server-Side Expiry Cancels Unconfirmed Holds
Given a hold exists for a slot and has not been confirmed When server time reaches the hold's expiry timestamp Then the server marks the hold Expired and releases the reservation And client attempts to confirm after expiry receive a 409 HOLD_EXPIRED response And the client UI reflects Expired state within 2 seconds via push or polling And no assignment is created for the expired hold
Hold Duration Respects Shift > Campaign > Org Defaults
Given hold duration is configurable at org, campaign, and shift levels And an org default exists (D_org), a campaign override may exist (D_campaign), and a shift override may exist (D_shift) When a new hold is created for a shift Then the effective hold duration is D_shift if present, else D_campaign if present, else D_org if present, else the system default And the effective expiry timestamp = server_now + effective_duration And changes to any duration setting apply to new holds created ≥1 minute after saving the setting
Timer Recovers Correctly After App Refresh or Navigation
Given a volunteer has an active hold When the app is refreshed, backgrounded, or force-closed and reopened before expiry Then the client fetches the hold from the server and rehydrates the countdown using the server expiry And the displayed remaining time matches server time within ±1 second And no duplicate holds are created during recovery And if the hold has already expired, the UI immediately shows Expired and disables Confirm
Offline Device Gracefully Reconciles Hold State
Given a volunteer starts a hold while online And the device goes offline before confirmation When the volunteer taps Confirm while offline Then the app queues the confirm request and labels it Pending Sync And upon reconnect, if the server has not reached the expiry timestamp, the hold is confirmed and the slot assigned And if the server has reached expiry, the confirm is rejected with HOLD_EXPIRED and the UI shows Expired within 2 seconds of reconnect And the client syncs to the authoritative server state on reconnect without creating duplicate assignments
Auto-Release Immediately Offers Slot to Next Candidate or Waitlist
Given an unconfirmed hold expires And assignment rules define next-candidate or waitlist behavior When the server marks the hold Expired Then the system attempts to reserve the slot for the next eligible candidate within 1 second And sends the appropriate notification to that candidate per channel rules And if no eligible candidate exists, the slot returns to the open pool immediately And all reservation operations are idempotent and concurrency-safe to prevent double assignment
Hold Lifecycle is Fully Logged for Audit
Given a hold is created for a slot When any state transition occurs (Created, Confirmed, Expired, Auto-Reserved to Next) Then an audit record is written containing: hold_id, slot_id, volunteer_id (if known), state, occurred_at (server time), expires_at, effective_duration, duration_source (org/campaign/shift/system), and request_id/correlation_id And audit records are queryable via admin API/UI within 60 seconds of the event And audit records are immutable once written And each record links to the resulting assignment when applicable
One-Tap Confirmation UX (Mobile-First)
"As a volunteer, I want to confirm my hold with one tap on my phone so that I can finish in seconds while on the go."
Description

Provide a frictionless, accessible confirmation screen optimized for mobile that summarizes shift details and finalizes assignment with one tap. On confirmation, commit the assignment, update rosters in real time, and trigger standard GiveCrew automations (receipt, calendar invite, reminder cadence). Support offline-friendly submission with background sync and clear success/failure states, including retry guidance if connectivity is poor.

Acceptance Criteria
Mobile Confirmation Screen Displays Shift Summary and CTA
Given a volunteer opens a valid Claim Hold link on a mobile device When the confirmation screen renders Then it displays role, date, start/end time, location, and organizer contact And it shows a visible countdown timer (mm:ss) for the hold window And a primary button labeled "Confirm" is visible above the fold and tappable with one tap And a secondary "Cancel" control is present and discoverable And the screen loads in ≤ 2.5 seconds at p75 on a 3G Fast network profile And body text is ≥ 16px and respects system font scaling up to 200% without clipping
Slot Hold Starts on Link Tap and Auto-Releases
Given the slot is available and a volunteer opens the Claim Hold link When the confirmation screen loads Then the slot is exclusively reserved for that volunteer for 3 minutes by default (configurable 1–10 minutes) And other users attempting to claim the same slot are prevented from confirming and see "Temporarily held" When the countdown reaches 0 without confirmation Then the hold auto-releases and the UI presents "Hold expired" with the Confirm button disabled And tapping Cancel immediately releases the hold and navigates back to the prior context And reopening the same link after release reflects the current availability state
One-Tap Confirmation Commits Assignment and Triggers Automations
Given an active hold for the volunteer When the volunteer taps the Confirm button once Then the assignment is committed exactly once (idempotent across retries) And the shift roster and slot count update in real time within 5 seconds And the standard automations are triggered: receipt sent, calendar invite issued, and reminder cadence scheduled within 60 seconds And the user sees a success state with clear next steps and the hold countdown disappears
Offline Confirmation Queues and Background Syncs
Given the device is offline or experiencing poor connectivity When the volunteer taps Confirm Then a signed confirmation intent is stored locally and a "Pending sync" success state is shown And the app retries sync in the background with exponential backoff for at least 15 minutes or until connectivity is restored And upon successful sync, the assignment is committed and automations fire without additional user action And if sync fails after 3 attempts or 15 minutes, a failure state appears with retry guidance and a "Try again" action And duplicate assignments are prevented across retries via a unique request identifier
Graceful Handling of Expired or Filled Slots
Given the hold has expired or the slot was filled by another user before confirmation When the volunteer attempts to confirm Then no assignment is created and a clear message explains the reason And the UI offers to re-hold the slot if available or shows at least 3 alternative shifts from the same campaign/day when possible And the user can return to the previous screen without data loss
Accessible Mobile UX Meets WCAG 2.2 AA
Given a user relies on assistive technologies or high-contrast settings When the confirmation screen is displayed Then all interactive elements have accessible names, roles, and states for VoiceOver/TalkBack And the countdown uses an ARIA live region with polite updates no more frequent than every 5 seconds And touch targets are ≥ 44x44 dp and color contrast is ≥ 4.5:1 for text and icons And the UI supports Dynamic Type up to 200% without truncation or loss of functionality And the flow is operable via external keyboard/switch access and contains no flashing content > 3 Hz
Real-Time Roster Update Visibility
Given an organizer has the roster view open for the shift When a volunteer confirms their assignment Then the roster reflects the new assignment within 5 seconds without manual refresh And any other active holds for that slot are cancelled and their UIs update accordingly And concurrent confirm attempts do not create double-assignments; later attempts receive clear feedback and no commit occurs
Organizer Controls: Extend, Release, Reassign
"As an organizer, I want to see and control active holds so that I can manage edge cases and keep shifts full."
Description

Expose an organizer dashboard panel listing all active holds with timers, claimant identity, and slot details. Allow authorized roles to extend a hold, force release it, or manually assign a different volunteer with appropriate confirmations and audit logs. Include bulk actions (e.g., release all expired holds) and guardrails (e.g., max extensions). Reflect changes instantly across clients and preserve a history for compliance and post-mortems.

Acceptance Criteria
View Active Holds Panel
Given an organizer with Hold Management permission is logged in and there are active holds, When they open the Organizer Controls panel, Then each hold row displays slot name, shift start time, claimant identity, hold start timestamp, remaining time countdown, and hold status. Given active holds are listed, When 1 second elapses, Then countdown timers update by 1 second without page refresh. Given no active holds exist, When the panel is opened, Then an empty-state message "No active holds" is shown. Given a hold has expired, When the panel is in view, Then the hold is visually flagged as expired and eligible for bulk release.
Extend Hold Within Guardrails
Given a hold is active and has remaining extensions below the configured maximum, When the organizer clicks Extend and confirms, Then the hold expiration increases by the configured increment and the remaining extensions decrement by 1. Given a hold has reached the maximum allowed extensions or would exceed the maximum total duration, When the organizer attempts to extend, Then the Extend control is disabled or a validation message prevents the action and no changes occur. Given an extend action is confirmed, Then an audit log entry records actor, hold ID, previous expiration, new expiration, timestamp, and reason if provided. Given a race condition where the hold expires before confirmation, When Extend is confirmed, Then the action is rejected with "Hold already expired" and no changes occur.
Force Release Hold
Given a hold is active, When the organizer clicks Release and confirms, Then the hold is immediately released, removed from the active list, and the associated slot becomes available for new claims. Given a release action is confirmed, Then an audit log entry records actor, hold ID, previous status, new status "released", and timestamp. Given the hold has already auto-released, When Release is attempted, Then the user sees a non-blocking notice "Hold already released" and no duplicate log is created.
Manual Reassign to Different Volunteer
Given a slot is currently held or assigned, When the organizer selects Reassign, searches, and selects a different volunteer, and confirms, Then the slot is assigned to the selected volunteer and any existing hold is released. Given the selected volunteer already has a conflicting assignment for the same time, When Reassign is attempted, Then the system blocks the action with a conflict message and no changes occur. Given a reassignment completes, Then an audit log entry records actor, slot ID, from volunteer if any, to volunteer, previous status, new status, and timestamp. Given the organizer selects the same volunteer as current, When Reassign is confirmed, Then the action is prevented as a no-op with a message.
Bulk Release Expired Holds
Given one or more holds are expired, When the organizer clicks Bulk Release Expired and confirms, Then all expired holds are released and removed from the list. Given some releases fail, When the operation completes, Then the result shows a per-hold success/fail summary and unaffected holds remain unchanged. Given bulk release runs, Then a single parent audit entry references per-hold child entries, each capturing actor, hold ID, outcome, and timestamp. Given no expired holds exist, When Bulk Release Expired is invoked, Then the system shows "No expired holds to release" and does not perform any action.
Real-Time Sync and History Preservation
Given two organizer clients are connected to the same organization, When any hold is extended, released, or reassigned on one client, Then the other client reflects the change within 2 seconds without manual refresh. Given an action occurs, Then the audit history is persisted and visible in the dashboard with actor, action, target hold or slot ID, before and after values, optional reason, and ISO 8601 UTC timestamp. Given an audit record exists, When it is queried via the dashboard, Then the record is immutable and returned exactly as stored.
Authorization and Guardrails Enforcement
Given a user lacks the Hold Management permission, When they attempt to view the Organizer Controls panel, Then they are denied with 403 or equivalent and no data is shown. Given a user lacks permission for a specific action, When they attempt Extend, Release, or Reassign, Then UI controls are hidden or disabled and any direct API call is rejected with 403 and logged. Given guardrail settings exist for max extensions per hold and max total hold duration, When organizers perform actions, Then the system enforces these limits consistently across UI and API. Given an unauthorized or blocked attempt occurs, Then an audit log entry records actor, attempted action, target, and reason "unauthorized" or "guardrail".
Notifications & Alerts for Holds
"As a volunteer, I want timely reminders and confirmations across SMS and email so that I don't miss my chance and know I'm scheduled."
Description

Send real-time notifications to volunteers when a hold starts, reminder pings before expiry, and confirmations or expiration notices. Provide optional organizer alerts for repeated expirations or when critical shifts reclaim capacity. Support SMS, email, and push with deduplication, quiet hours, localization, and template management. Integrate with existing GiveCrew messaging and respect user communication preferences.

Acceptance Criteria
Volunteer Real-Time Hold Start Notification
- Given a volunteer taps a Claim link and an active hold is created, When the volunteer has at least one enabled channel per their preferences, Then exactly one hold-start notification is sent via the highest-priority available channel (Push > SMS > Email) within 5 seconds of hold creation. - And no duplicate notifications for the same hold-start event are sent across channels or retries. - And the message content includes shift name, shift date/time in the volunteer’s timezone, time-to-expiry (countdown or timestamp), and a one-tap Confirm link. - And delivery attempt, channel, template ID, locale, and correlation ID are logged via GiveCrew Messaging.
Volunteer Pre-Expiry Reminder Ping
- Given an active, unconfirmed hold, When remaining hold time equals the configured reminder offset (default 2 minutes), Then send exactly one reminder via the highest-priority available channel within ±10 seconds of the offset. - And do not send the reminder if the hold is already confirmed or released at the offset time. - And the reminder includes remaining time and the one-tap Confirm link. - And no more than one reminder is sent per hold.
Volunteer Confirmation and Expiration Notices
- Given a volunteer confirms the hold while it is active, When confirmation is received, Then send a confirmation receipt via the volunteer’s highest-priority available channel within 5 seconds, cancel any scheduled reminders, and record the assignment. - Given a hold expires unconfirmed, When expiry occurs, Then send an expiration notice via the highest-priority available channel within 10 seconds that includes a link to view other open slots. - And no confirmation is sent after expiration, and no expiration notice is sent after confirmation. - And all events are logged with correlation to the hold ID.
Organizer Alerts for Repeated Volunteer Expirations
- Given the organization has Repeated Expiration Alerts enabled with threshold N=3 within window W=7 days, When a volunteer accrues N expired holds within W, Then send a single alert to the designated organizer recipients via GiveCrew Messaging within 60 seconds. - And subsequent expirations within the same window do not trigger additional alerts for that volunteer until the window resets. - And the alert includes volunteer name, count and timestamps of expirations, and links to view profile and outreach options. - And alert delivery respects organizer channel preferences and quiet hours.
Organizer Alert When Critical Shift Reclaims Capacity
- Given a shift is marked Critical and a hold on that shift expires, When the expiration increases available capacity, Then send an organizer alert within 15 seconds containing the shift details and a Quick-Fill link. - And do not send more than one alert per shift within a 5-minute suppression window for repeated capacity reclaims. - And alert delivery respects organizer channel preferences and localization.
Quiet Hours Enforcement and Stale Notification Suppression
- Given the recipient is within their quiet hours for a channel, When a hold-related notification would be sent via that channel, Then suppress that channel and attempt the next allowed channel; if all channels are in quiet hours, do not send any outbound message. - And if a suppressed notification would be stale by the end of quiet hours (e.g., the hold will have expired), it is dropped and not sent later; otherwise it is queued and sent when quiet hours end. - And all suppressions and drops are logged with reason codes.
Localization and Template Management for Hold Notifications
- Given a recipient’s locale and timezone are known, When generating any hold-related message, Then select the org-approved template matching the locale; if unavailable, fall back to en-US. - And render all placeholders without missing tokens; date/time fields are formatted in the recipient’s timezone and locale. - And each message includes required compliance elements per channel (sender ID, unsubscribe/opt-out instructions) and stays within channel limits (SMS <= 3 segments; email subject <= 100 chars). - And the template ID and version are logged; only published templates can be used.
Hold Analytics & Impact Board Metrics
"As a program lead, I want metrics on holds and conversions so that I can tune the window and messaging to improve show-up rates."
Description

Instrument the hold funnel to capture holds created, confirm rate, time-to-confirm, expirations, and channel performance. Surface KPIs on the Impact Board with drill-down by campaign, shift type, and timeframe. Provide CSV export and an internal dashboard for experimentation (e.g., comparing different hold windows or reminder timings). Use insights to recommend optimal hold duration and messaging to improve show-up rate.

Acceptance Criteria
Instrument Hold Funnel Events
Given a volunteer taps a valid claim link for a shift When the system creates a hold Then an event hold_created is recorded exactly once with fields: hold_id, volunteer_id_hash, shift_id, campaign_id, shift_type, channel, hold_window_seconds, message_variant_id, experiment_id, created_at_utc Given a held shift is confirmed within the window When the volunteer taps Confirm Then hold_confirmed is recorded exactly once with confirmed_at_utc and derived time_to_confirm_seconds And confirm_rate for the selected timeframe equals confirmed/created within ±0.5% of transactional counts Given a held shift is not confirmed before window expiry When the window elapses Then hold_expired is recorded exactly once with expired_at_utc And the hold is auto-released and recorded as hold_auto_released Given a coordinator manually releases a hold before expiry When they submit the release action Then hold_released_manual is recorded with released_at_utc and release_reason Given the event pipeline retries after a transient failure When messages are replayed Then duplicate events are not created for the same hold_id (idempotent processing)
Impact Board KPIs and Drill-down
Given the Impact Board is opened with default timeframe This Week When data loads Then the board displays holds_created, holds_confirmed, confirm_rate, median_time_to_confirm_seconds, holds_expired, and channel breakdown Given a user filters by Campaign, Shift Type, and Last 30 Days When filters are applied Then all KPIs and charts update within 2 seconds P95 And values match backend aggregates within ±0.5% Given new hold events occur When within 60 seconds Then the Impact Board reflects updated counts (data freshness ≤ 60s P95) Given the user clicks a channel segment in a chart When drill-down is invoked Then the view filters to that channel and preserves existing filters Given the board is displayed in public mode When rendering metrics Then no volunteer PII appears; only aggregate metrics are shown
CSV Export of Hold Metrics
Given filters are applied on campaign, shift type, channel, and timeframe When the user exports Events CSV Then a file is generated within 60 seconds containing one row per event with columns: event_type, hold_id, shift_id, campaign_id, shift_type, channel, hold_window_seconds, message_variant_id, experiment_id, event_timestamp_utc, time_to_confirm_seconds (nullable), release_reason (nullable) And the row count equals the number of events matching the filters Given the user exports Summary CSV When the export completes Then the file contains aggregated rows per day (or selected grain) with holds_created, holds_confirmed, confirm_rate, median_time_to_confirm_seconds, holds_expired And all timestamps are UTC and numerics use dot decimal Given an export would exceed 100k rows When the user requests it Then the system paginates and streams the download without timeout and shows progress Given a user without Export permission attempts to export When they click Export Then the action is blocked and an explanatory message is shown
Internal Experimentation Dashboard
Given experiments vary hold_window_seconds and reminder timing across cohorts When an analyst opens the Experiments dashboard Then each experiment shows cohorts, sample sizes, confirm_rate, median time_to_confirm, and expirations And differences in confirm_rate include 95% confidence intervals and significance indicators Given an experiment has fewer than 200 holds per arm or has run less than 7 days When viewed Then its status is Collecting Data and no winner is called Given overlapping experiments target the same audience/timeframe When detected Then the dashboard flags a conflict with impacted cohorts listed Given a user selects a metric and timeframe When filters are applied Then results recompute within 2 seconds P95 and match underlying aggregates within ±0.5%
Data Quality, Latency, and Reconciliation
Given the UTC day boundary is reached When daily reconciliation runs Then analytics aggregates match transactional counts for holds_created, holds_confirmed, and holds_expired within ±0.5% or an alert is raised Given an ingestion backlog occurs When pipelines recover Then backfill completes within 2 hours for up to 1,000,000 events and preserves per-hold_id ordering And reprocessing is idempotent with no duplicate counts Given client clocks are skewed When events are received Then server-assigned timestamps are used for created_at_utc to ensure consistency Given volunteer identifiers exist When events are stored in analytics Then volunteer_id is hashed and no names, emails, or phone numbers are stored
Recommendations for Hold Duration and Messaging
Given at least 1,000 holds per channel or 30 days of data (whichever comes first) are available When the nightly recommendation job runs Then it outputs per channel and shift_type an optimal hold_window_seconds and top message_variant_id with expected confirm_rate uplift and 95% confidence bounds Given data volume is below thresholds When the job runs Then recommendations are marked Insufficient Data with the missing sample size shown Given a coordinator views Recommendations When a recommendation is available Then they can preview the rationale and accept or dismiss it, and the decision is audit-logged with timestamp and user id
Access Control and Privacy for Metrics
Given a user with role Coordinator signs in When they open the Impact Board Then they can view KPIs and drill-down but cannot access the Internal Experimentation Dashboard or Export CSV Given a user with role Admin or Analyst signs in When they open Analytics Then they can access the Internal Experimentation Dashboard and Export CSV Given an export is generated When a download link is created Then it is a signed URL valid for 24 hours and each access is audit-logged

ETA Filter

Calculates real travel time before texting and only pings volunteers who can arrive before your cutoff. Captains see live ETAs and “on the way” statuses, reducing wasted outreach and improving start-time reliability.

Requirements

Location Consent & Source Integration
"As a volunteer, I want to control and share my location for shifts so that the app can calculate if I can arrive on time without compromising my privacy."
Description

Capture and use volunteer location data in a privacy-safe way to power ETA calculations. Support multiple origin sources (GPS via mobile web/app, saved home/work address or ZIP, last check-in location), with explicit opt-in, clear purpose disclosure, and easy opt-out. Implement minimal data retention, encryption at rest/in transit, consent timestamping, and audit logs. Integrate with a geocoding/routing provider (e.g., Mapbox/Google/OSM), normalize time zones, and set accuracy thresholds. Provide graceful fallbacks when location is unavailable (e.g., ZIP centroid) and regional restrictions handling. Expose APIs for reading/updating preferred origin and cache results to reduce external API calls.

Acceptance Criteria
Explicit Location Consent with Purpose Disclosure
Given a volunteer attempts to use ETA features without prior location consent When location is first required Then the app displays a consent modal that includes: purpose of use (ETA calculation), data sources, retention window (<=24h for precise samples), and provider disclosure, with primary actions "Allow" and "Decline", default focus on "Decline" And no location data is accessed or stored until "Allow" is selected And upon "Allow", a consent record is stored with fields: user_id, scope="ETA", platform, policy_version, disclosure_hash, consent_timestamp (UTC), region_code Given the volunteer selects "Decline" or closes the modal Then precise location is not requested and the system proceeds to fallbacks only Given the volunteer visits Settings > Privacy and selects "Revoke Location Consent" When revocation is confirmed Then all stored precise location samples for that user are deleted within 60 seconds and subsequent ETAs use non-precise sources And an audit_log entry "consent_revoked" is written with user_id, timestamp (UTC), and previous scope
Preferred Origin Source Selection and Hierarchy
Rule: Origin selection order is: PreferredOrigin (if set) > Current GPS (if consented and accuracy <=100m and sample_age <=5m) > LastCheckIn (<=30d and accuracy <=200m) > Saved Home/Work address > Saved ZIP > Prompt for ZIP > DoNotText Given a user has set a PreferredOrigin to "Home" When an ETA is calculated Then the origin used equals the Home address geocode Given GPS is available and meets thresholds and the user selects "Use current location" When an ETA is calculated Then the origin used equals the current GPS coordinate rounded to 5 decimals Given no usable origin source exists When an ETA is requested Then the volunteer is prompted for ZIP entry and no text is sent until a usable origin exists
Routing Provider Integration and Caching
Given Mapbox (or configured provider) is set as the routing provider When an ETA is requested with origin O and destination D Then the system requests a driving ETA with live traffic and returns a duration in seconds Given 100 identical ETA requests for the same O and D within 60 seconds When the requests are processed Then external routing provider calls are <=5 and cache hit rate is >=95% Given the routing provider returns a 5xx error When three retry attempts with exponential backoff fail Then the system marks ETA as unavailable and no outbound text is sent for that volunteer And an audit_log "routing_error" entry is recorded with provider, status_code, and request_id
Time Zone Normalization
Given an event with timezone America/New_York and a volunteer device in America/Los_Angeles When an ETA is computed at 13:00 local device time on 2025-03-09 Then the server stores all operation timestamps in UTC and returns the ETA arrival displayed in America/New_York local time with correct DST handling And API responses include ISO 8601 timestamps with timezone offsets (e.g., 2025-03-09T21:00:00Z) Given an ETA spans a DST change When displayed in the event timezone Then the arrival time reflects the post-change offset as per the timezone database
Regional Restrictions and Accuracy Thresholds
Given the device denies precise location or OS "Precise Location" is off When the app requests location Then only coarse location is requested and stored, and the system falls back to ZIP centroid or saved address And a reason_code "PRECISE_DENIED" is recorded in the audit_log Given the user is in a region flagged "NoPreciseLocation" by configuration When entering the ETA flow Then GPS-based options are disabled and the user is prompted for manual origin Given a location sample with accuracy >1000m or age >10m When origin selection runs Then the sample is rejected and the next source in the hierarchy is used
Data Security, Retention, and Audit Logging
Given API requests to location endpoints When made over HTTP Then they are rejected or redirected to HTTPS and no data is processed And TLS version is >=1.2 as verified by integration test Given a precise location sample saved at T0 When the retention job runs after T0+24h Then the sample is purged and only aggregated or preference data remains Given user actions: consent_given, consent_revoked, origin_set, origin_updated, routing_call When these actions occur Then an immutable audit_log entry is written with user_id (or system), action, timestamp (UTC), and minimal metadata (coordinates rounded to 3 decimals or masked), accessible only to org admins And database/storage configuration evidences encryption at rest for tables/columns storing coordinates and addresses
Public API for Preferred Origin
Given an authenticated client with scope "volunteer:origin:read" When it calls GET /api/v1/volunteers/{id}/origin Then the response is 200 with JSON fields: preferred_origin_type, address_or_coord, source, updated_at (ISO 8601), etag Given scope "volunteer:origin:write" When it calls PUT /api/v1/volunteers/{id}/origin with a valid payload Then the response is 200, the origin is updated, an audit_log "origin_updated" is written, and subsequent ETA calculations use the new origin Given an If-Match ETag that does not match current When PUT is called Then the response is 412 Precondition Failed and no update occurs Given an invalid payload (e.g., malformed coordinates) When PUT is called Then the response is 400 with a machine-readable error code and field-level messages
Arrival Cutoff & Buffer Configuration
"As a captain, I want to set an arrival cutoff and buffer for each event so that only volunteers who can make it in time are contacted."
Description

Enable captains to define event-level latest acceptable arrival time with configurable buffers (e.g., arrive by 5:50 PM for a 6:00 PM shift). Provide org-level defaults and per-event overrides, including per-role variations (e.g., setup crew vs greeters). Validate against event start/end times, handle time zones, and persist settings with change history. Expose settings via UI and API, and ensure these parameters are consumed by the ETA filter and messaging workflows.

Acceptance Criteria
Org Defaults Inherit To New Event
Given an organization has a default arrival buffer of 10 minutes and cutoff enforcement enabled When a captain creates a new event with start time 2025-09-12T18:00:00 in timeZoneId "America/Los_Angeles" Then the event’s computed latestAcceptableArrival is 2025-09-12T17:50:00-07:00 and the UI preview shows "Arrive by 5:50 PM" labeled with the event time zone And the event inherits the org defaults for all roles without explicit overrides When the captain changes the event-level buffer to 15 minutes and saves Then the computed latestAcceptableArrival updates to 2025-09-12T17:45:00-07:00 and persists after page reload and via GET /events/{id}/arrival-policy
Per-Role Buffers With Event Overrides
Given roles Setup and Greeter exist for an event starting at 2025-09-12T18:00:00 America/Los_Angeles and org defaults are Setup=30 minutes, Greeter=10 minutes When the captain overrides only Setup to 45 minutes at the event level and saves Then the computed latestAcceptableArrival values are Setup=2025-09-12T17:15:00-07:00 and Greeter=2025-09-12T17:50:00-07:00 And the Event Settings UI displays each role with its buffer and computed "arrive by" time And GET /events/{id}/arrival-policy returns perRole buffers and computed latestAcceptableArrival values for each role
Buffer Validation Against Event Times
Rule: BufferMinutes must be a non-negative integer; non-integer, negative, or empty inputs are rejected client-side and server-side with error code 400 and message "Buffer must be a whole number of minutes ≥ 0" Rule: Computed latestAcceptableArrival must be less than or equal to the event start time; values resulting in a later arrival are rejected Given an event with start 2025-09-12T18:00:00 When the captain inputs BufferMinutes = -5 or 10.5 Then the Save action is disabled in the UI and POST/PUT returns 400 with the above error message When the event start time is changed Then all computed "arrive by" times for each role recompute immediately and update in the UI preview before save
Time Zone and DST Correctness
Given an event timeZoneId of "America/Los_Angeles" and start 2025-08-15T18:00:00 When BufferMinutes = 10 Then GET /events/{id}/arrival-policy returns latestAcceptableArrival "2025-08-15T17:50:00-07:00" and timeZoneId "America/Los_Angeles" Rule: Computation of latestAcceptableArrival is eventStart minus BufferMinutes evaluated in the event’s IANA time zone and remains correct across Daylight Saving Time transitions Rule: The UI displays event times labeled with the event time zone; changing the editor’s local time zone does not change persisted values When an editor in a different time zone views the event Then the API continues to persist times in the event time zone and the UI indicates the event’s time zone next to the displayed times
Persistence and Change History Audit
Given an event with an existing arrival policy When a captain changes the Greeter buffer from 10 to 15 minutes via the UI and saves Then a change history entry is recorded with fields: eventId, changedBy (userId), occurredAt (UTC ISO8601), field (Greeter.bufferMinutes), before (10), after (15), source (UI), requestId And GET /events/{id}/arrival-policy/history returns the new entry at index 0 (most recent first) When the same change is made via the API Then source is recorded as API and changedBy reflects the API client identity Rule: Reverting to a previous value creates a new history entry; history is immutable
API: Get/Set Arrival Policy
Given a valid admin-scoped API token When calling GET /orgs/{orgId}/arrival-policy Then the response is 200 with JSON containing default bufferMinutes and optional perRole defaults When calling GET /events/{eventId}/arrival-policy Then the response is 200 with JSON containing perRole bufferMinutes overrides (if any), inherited flags, and computed latestAcceptableArrival (ISO8601 with offset) per role and the event timeZoneId When calling PUT /events/{eventId}/arrival-policy with valid body { perRole: { Greeter: { bufferMinutes: 15 } } } Then the response is 200 and subsequent GET reflects the update When calling the same PUT with invalid values (e.g., -1, non-integer) Then the response is 400 with a validation error object and no changes are persisted Rule: All API responses include ETag; conditional updates with If-Match enforce concurrency control, returning 412 on version mismatch
ETA Filter and Messaging Respect Cutoff
Given an event start at 2025-09-12T18:00:00 America/Los_Angeles with Greeter buffer 10 minutes (latestAcceptableArrival 17:50) And three candidates with ETAs: A=17:42, B=17:50, C=17:55 (event time zone) When the ETA filter runs Then A and B are included in the outreach queue; C is excluded with reason "excluded_due_to_arrival_cutoff" When the Greeter buffer is changed to 20 minutes (latestAcceptableArrival 17:40) Then on the next ETA filter cycle (≤ 1 minute), A and C are excluded and existing queued messages for A are canceled if unsent Rule: Outbound invites/reminders include the computed "Arrive by" time labeled with the event time zone; if current time is past the latestAcceptableArrival for a role, new recruitment messages for that role are not sent and a warning is logged
Traffic-aware ETA Computation Engine
"As an organizer, I want accurate, real-time ETAs to my event location so that I can reliably assess who can arrive before the cutoff."
Description

Build a service that computes real-time ETAs from volunteer origin to event location using traffic-aware routing. Support transport modes (driving, transit, walking, biking), arrival-by vs depart-now logic, and current conditions. Implement batching of multi-origin requests, rate limiting, retries, and fallbacks to historical median travel times when live data is unavailable. Return distance, ETA, and confidence score. Monitor provider quotas and failures, and target sub-500 ms p95 latency for single-origin calls.

Acceptance Criteria
Depart-Now Single-Origin Driving ETA (Traffic-Aware, p95 <= 500ms)
Given a single origin and destination with mode=driving and depart=now And live traffic data is available from the routing provider When the engine computes the ETA Then it returns distance_meters, duration_seconds, arrival_timestamp, and confidence (0.0–1.0) And the route uses traffic-aware parameters per provider capability And measured p95 latency across >= 1000 single-origin requests is <= 500 ms
Multi-Mode Depart-Now ETA (Driving, Transit, Walking, Biking)
Given an origin and destination with mode in {driving, transit, walking, biking} and depart=now When the engine computes the ETA Then the provider is invoked with the correct mode parameter for the request And for each supported mode, a valid response with distance_meters, duration_seconds/arrival_timestamp, and confidence is returned And walking/biking routes exclude motorways; transit routes adhere to current schedules; driving uses road network with traffic And if an unsupported mode is requested, the engine returns a validation error without calling the provider
Arrival-By ETA Calculation and Depart-Time Derivation
Given an origin, destination, transport mode, and an arrival_by timestamp (ISO 8601, timezone-aware) When the engine computes an arrival-by ETA Then it returns depart_timestamp and arrival_timestamp such that arrival_timestamp <= arrival_by And it prefers provider arrival-by routing when supported; otherwise it derives depart_timestamp by searching depart-now estimates And if no route can arrive by the target time for the given mode, the engine returns a clear ROUTE_UNAVAILABLE error
Fallback to Historical Median When Live Data Unavailable
Given that live traffic-aware data is unavailable or provider calls return 5xx/429/timeouts When the engine computes the ETA Then it falls back to historical median travel time for the same origin–destination, mode, and matching time-of-day/day-of-week bucket And it returns distance_meters, duration_seconds, arrival_timestamp, and a confidence that is lower than for live-traffic results And the fallback event and reason are logged for observability
Batching and Rate Limiting for Multi-Origin Requests
Given a request containing up to 100 origins for the same destination and mode When the engine computes batch ETAs Then provider calls are chunked into batches of size <= configured batch_size And the outbound request rate does not exceed the configured qps_limit (verified via metrics) And each origin receives an independent success result or explicit error without blocking other origins And unexpected provider 429 responses trigger retry with backoff without exceeding qps_limit
Retries with Exponential Backoff and Fallback on Exhaustion
Given provider calls fail with transient errors (5xx, timeouts) or rate limits (429) When the engine computes ETAs Then each failed call is retried up to configured max_retries using exponential backoff with jitter And successful retry responses are returned transparently to the caller And after max_retries are exhausted, the historical fallback path is applied And retry counts and outcomes are captured in metrics and structured logs
Monitoring of Provider Quotas, Errors, and Latency SLOs
Given normal and peak traffic conditions When the engine processes ETA requests Then it emits metrics including request_count, success_count, error_count, fallback_count, p50_latency_ms, p95_latency_ms, provider_quota_remaining, and retry_count And dashboards and alerts exist with configurable thresholds for quota utilization and error rates And an alert triggers when single-origin p95 latency exceeds 500 ms over a 5-minute window And provider failures and quota breaches are traceable via correlation IDs across logs and metrics
Eligibility-Filtered Outreach Messaging
"As a captain, I want outreach to automatically target only volunteers who can arrive on time so that we reduce wasted texts and improve show-up."
Description

Before sending outreach texts, filter the candidate pool to those whose computed ETA is at or below the arrival cutoff. Combine ETA with proximity, availability, past reliability, and cooldown rules to prioritize who to ping. Respect quiet hours, opt-outs, and contact preferences. Generate personalized SMS templates that include event details and a deep link to confirm/"On my way". Throttle sends to manage provider limits, deduplicate contacts, and log all decisions for analytics on reach, conversions, and no-shows.

Acceptance Criteria
ETA Eligibility Filtering Before Outreach
Given an event at 123 Main St with start time 10:00 and arrival cutoff 09:50 (event local timezone) And volunteer V1 has computed ETA = 9 minutes And volunteer V2 has computed ETA = 14 minutes And volunteer V3 has no geolocation (ETA unavailable) When eligibility is evaluated at 09:30 local time Then V1 is marked Eligible And V2 is marked Ineligible with reason code ETA_ABOVE_CUTOFF And V3 is marked Ineligible with reason code ETA_UNKNOWN And only Eligible volunteers proceed to prioritization and messaging Given V1’s ETA increases to 12 minutes at 09:33 When the system recomputes ETAs immediately prior to send at 09:33 Then V1 is marked Ineligible with reason code ETA_ABOVE_CUTOFF and is not messaged
Multi-Factor Prioritization and Cooldown Enforcement
Given Eligible volunteers E1, E2, and E3 with attributes: - E1: ETA 6 min, distance 2 km, availability = Yes, reliability score = 0.6, cooldown = inactive, last_ping = 7 days ago - E2: ETA 8 min, distance 5 km, availability = Yes, reliability score = 0.9, cooldown = inactive, last_ping = 30 days ago - E3: ETA 4 min, distance 1 km, availability = Yes, reliability score = 0.8, cooldown = active When prioritization runs Then E3 is excluded with reason code COOLDOWN_ACTIVE And remaining candidates are ordered by: ETA ascending, then reliability descending, then distance ascending, then last_ping oldest first And the resulting send order is [E1, E2] And the prioritization decision (order and reasons) is persisted per candidate
Quiet Hours, Opt-Outs, and Contact Preferences
Given organization quiet hours configured as 21:00–08:00 (event local timezone) And volunteers P1 (SMS allowed), P2 (opted out of SMS), P3 (prefers Email only) And outreach is triggered at 07:50 on event day When eligibility and compliance checks run Then no SMS are sent before 08:00 And P2 is excluded from SMS with reason code OPTOUT_SMS And P3 is excluded from SMS with reason code PREFERS_NON_SMS And messages for P1 are scheduled for 08:00 if still Eligible at send time And if earliest allowed send time would cause ETA > arrival cutoff, the volunteer is excluded with reason code MISSED_CUTOFF_AT_ALLOWED_SEND
Personalized SMS with Event Details and Deep Link
Given event "Canvass Downtown" at 123 Main St starting Sat 10:00 (event local timezone) And volunteer record has first_name = Alex and normalized phone = +15551234567 When composing the outreach SMS Then the message body includes: greeting with "Alex", event title, start time, short address, computed ETA minutes, and a unique deep link of the form https://givecrew.app/join?e=<event_id>&v=<volunteer_id>&t=<signed_token> And all placeholders are resolved (no unreplaced tokens) And the initial message length is <= 320 characters And the deep link resolves with HTTP 200 and loads a confirmation screen And the message is stored with a template_id and rendered_body for audit
Send Throttling, Provider Limits, and Deduplication
Given provider rate limits of 10 SMS/second per sender ID And a prioritized list of 50 recipients including two records with the same normalized phone +15557654321 When the send job executes at 09:32 Then the system emits at most 10 SMS per second per sender ID And duplicate phone numbers are deduplicated so only one SMS is sent to +15557654321 And each send uses an idempotency key (campaign_id + volunteer_id) so rerunning the job does not produce duplicate messages And 429/5xx responses are retried up to 3 times with exponential backoff (1s, 2s, 4s) And a send report records counts for sent, throttled, retried, failed
Decision Logging and Analytics (Reach, Conversions, No-Shows)
Given an outreach run with 100 candidates where 60 are messaged, 20 confirm "On my way" via deep link, and 5 of those 20 are later marked no-show When analytics are computed 1 hour after event start Then a decision log exists for 100/100 candidates including eligibility_decision, reason_codes, and input features (ETA, proximity, availability, reliability, cooldown) And delivery statuses are recorded for >= 95% of sent messages within 10 minutes of send (subject to provider callbacks) And metrics are produced: reach = 60, conversions = 20, conversion_rate = 33.33%, no_shows = 5, no_show_rate_among_converters = 25% And logs and metrics are retrievable via API and visible on the Impact Board
Deep Link Confirmation Triggers "On the Way" and Live ETA
Given a volunteer receives an SMS with a confirmation deep link When they tap the link and confirm attendance Then their status updates to "On the way" within 5 seconds of confirmation And their live ETA is recalculated from current device location when permission is granted, else from saved address with an "approx" flag And the Captain dashboard displays the volunteer with "On the way" and ETA within 10 seconds of confirmation And all confirmation events are logged with timestamp, location_source (device|address), and computed ETA
Volunteer On-My-Way Confirmation & Status Updates
"As a volunteer, I want an easy way to confirm I'm on my way and update my status so that captains know when to expect me."
Description

Provide volunteers a one-tap "On my way" flow via SMS deep link or in-app, updating their status and initiating lightweight, periodic ETA refreshes. Offer alternatives like "Running late" and "Can't make it" with reasons. Implement geofencing to auto-transition to "Arrived" within a defined radius, with manual override options. Minimize battery and data usage, protect privacy (only route-level signals, no continuous tracking), and send acknowledgments/updates as needed if ETA drifts past the cutoff.

Acceptance Criteria
SMS Deep Link One‑Tap On My Way
Given a volunteer receives an SMS deep link tied to a specific shift and cutoff time and the link is valid (≤6 hours old and before shift start), When the volunteer taps the link, Then the app or mobile web opens the confirmation screen in ≤3 seconds showing location, start time, and cutoff. Given the volunteer confirms "On my way", When the confirmation is submitted, Then the server records status = "On the way" in ≤5 seconds or queues offline and syncs within ≤30 seconds of connectivity returning, and a confirmation SMS is sent once. Given status is updated server-side, Then the captain dashboard displays the volunteer as "On the way" with an initial ETA in ≤10 seconds. Given the deep link is tapped again or the confirmation is retried, Then the operation is idempotent (no duplicate entries or duplicate SMS) and the original timestamp is preserved. Given the deep link is expired, revoked, or for a different shift, When tapped, Then an error screen explains the issue with an action to open the correct upcoming shift and no status change occurs.
In‑App On My Way Confirmation & Periodic ETA Refresh
Given a volunteer views an upcoming shift (≤24 hours) in-app and has location permission, When they tap "On my way", Then ETA estimation starts using OS routing APIs with coarse origin and destination and schedules refresh every 5 minutes or on significant-location-change (≥500m), without streaming raw coordinates to the server. Given status = "On the way", When 5 minutes elapse or a significant change occurs, Then a fresh ETA is computed and sent; total background data usage remains ≤150 KB/hour and ETA updates are limited to ≤12 per hour. Given the volunteer is stationary for ≥10 minutes before departure, Then ETA refreshes are paused until movement resumes, with at most one 15-minute heartbeat update containing no precise location. Given device battery <15% or OS Low Power Mode is enabled, Then the refresh interval backs off to every 10 minutes and pauses entirely when app is backgrounded. Given the volunteer denies location permission, When they tap "On my way", Then a fallback prompt collects a manual ETA (5/10/15/30 min preset or custom) and the system proceeds using that ETA.
Running Late With Reason & ETA Recalculation
Given status = "On the way", When the volunteer selects "Running late", Then the UI requires a reason (picklist or free text 2–120 chars) and confirms a new ETA (auto-suggested by routing or manual entry). Given a new ETA is provided, Then server updates status = "Running late", persists the reason, and updates ETA; captain dashboard reflects the change in ≤10 seconds and shows the reason tooltip. Given the recalculated ETA exceeds the shift cutoff, Then the volunteer is prompted to either keep coming as late or cancel; if they choose to keep coming, the board tags "Late (ETA +mm)"; if cancel, flow transitions to "Can't make it" with the same reason carried over. Given the volunteer does not respond to the running-late prompt within 2 minutes, Then a single reminder SMS is sent; no more than 1 reminder is sent per shift for running-late.
Can’t Make It With Reason & Backfill Trigger
Given a volunteer opens the status menu, When they select "Can't make it", Then the UI requires a reason (picklist or free text 2–120 chars) and optional notes, and confirms cancellation. Given cancellation is confirmed, Then server sets status = "Cancelled" in ≤5 seconds, records the reason, frees the slot, and posts a captain notification in ≤10 seconds. Given a slot is freed, Then the system triggers backfill outreach only to volunteers whose ETA Filter predicts arrival before cutoff, and sends at most one outreach per candidate within 15 minutes. Given the volunteer cancels via SMS keyword (e.g., "CAN'T"), Then the same cancellation and backfill logic executes and a confirmation SMS is sent.
Geofenced Auto‑Arrived With Manual Override
Given a shift has a defined arrival geofence radius of 100 meters, When the volunteer device reports entry with 2 consecutive geofence events or ≥1 minute dwell inside the radius, Then status auto-transitions to "Arrived" and periodic ETA refreshes stop. Given status auto-transitions to "Arrived", Then captain dashboard reflects "Arrived" in ≤10 seconds and the volunteer receives a single arrival acknowledgment. Given a false-positive arrival (e.g., drive-by), When the volunteer taps "Still on the way" within 5 minutes, Then status reverts to "On the way" and geofence auto-arrival is suppressed for the next 10 minutes. Given the volunteer has privacy mode (no precise location) enabled, Then geofenced arrival is disabled and the UI offers a manual "Mark Arrived" button; tapping it sets status = "Arrived" and stops ETA refreshes.
Privacy & Route‑Level Signals Only
Given any status or ETA update is sent, Then no raw latitude/longitude or continuous location traces are persisted server-side; only status transitions, ETA timestamp, and distance bucket (0–1mi, 1–3mi, 3–10mi, >10mi) are stored. Given location permission is requested, Then a one-time disclosure explains that only route-level signals (ETA and distance bucket) are used and no continuous tracking occurs; refusal still allows SMS-based flows. Given an auditor inspects telemetry, Then there are no database fields containing precise coordinates and no update frequency exceeding once per minute; ephemeral location used for routing is kept in-memory only and discarded after use. Given data retention policies run nightly, Then route-level ETA snapshots older than 30 days are purged; status transitions remain per org retention policy and contain no precise location.
ETA Drift Past Cutoff Notifications & Captain Updates
Given status = "On the way", When computed ETA exceeds the cutoff by >3 minutes, Then the system flags the volunteer as "Past cutoff ETA", sends the volunteer an SMS prompt to confirm "Running late" or "Can't make it" within 30 seconds, and marks the board with a red indicator. Given the volunteer confirms "Running late" and ETA ≤ cutoff + 20 minutes, Then status remains active with "Late" tag; otherwise if ETA > cutoff + 20 minutes, the system recommends cancellation and backfill. Given the volunteer does not respond to the prompt, Then no more than 2 total prompts are sent per shift, spaced ≥10 minutes apart; outreach to that volunteer pauses until they respond. Given ETA later improves to ≤ cutoff, Then the red indicator clears, the "Late" tag is removed, and normal refresh cadence resumes.
Captain Live ETA Dashboard & Alerts
"As a captain, I want to see live ETAs and who's en route so that I can start on time and adjust outreach accordingly."
Description

Add a real-time view for captains showing a map/list of invited, eligible, contacted, confirmed, and "on the way" volunteers with their ETAs, distances, and risk flags when an ETA exceeds the cutoff. Provide filters, sorting, and aggregate counts, plus visual/audio alerts for new confirmations and at-risk arrivals. Ensure mobile-first UI, 30-second auto-refresh (or websockets), resilience to intermittent connectivity, and integration with the Impact Board to display en-route and arrival metrics.

Acceptance Criteria
Live ETA Map/List View for Captain (Mobile-First)
Given a scheduled shift with at least one invited volunteer and ETA/location data available When the captain opens the Live ETA Dashboard Then the default view is a list grouped by status (Invited, Eligible, Contacted, Confirmed, On The Way) And each row shows volunteer name, current status, ETA in minutes, distance in device-locale units, and a last-updated timestamp And a Map toggle displays pins color-coded by status with ETA labels; tapping a pin opens the corresponding list row And list and map views remain in sync for selection and data And the first contentful paint occurs within 2 seconds on a mid-tier Android device over 4G And all tap targets are at least 44x44 px and text meets WCAG AA contrast
ETA Cutoff Risk Flagging
Given a shift arrival cutoff is configured (e.g., 15 minutes before start) and calculated ETAs are available When a volunteer’s ETA exceeds the cutoff threshold Then the volunteer is marked At Risk with a red badge and included in the At Risk list and count And when the volunteer’s ETA later becomes less than or equal to the cutoff, the At Risk badge and count clear automatically And the risk evaluation runs on every update cycle and within 5 seconds of any ETA change And the current cutoff value is visible in the dashboard header
Filtering and Sorting Controls
Given the dashboard with populated volunteers When the captain applies filters (Status, At Risk, ETA window, Distance range, Skill/Tag) Then only volunteers matching all selected filters are shown And the default sort is ETA ascending, with options for Distance ascending and Status priority (On The Way > Confirmed > Contacted > Eligible > Invited) And changing sort updates the list within 300 ms for up to 500 volunteers And clearing filters/sort restores defaults And the last used filters/sort persist per captain for the shift for 24 hours on the device
Aggregate Counts Accuracy
Given any combination of volunteer statuses on the dashboard When the list is rendered or updated Then the aggregate counters for Invited, Eligible, Contacted, Confirmed, On The Way, and At Risk exactly match the number of volunteers in each status (no double counting) And totals update within 5 seconds of any status or ETA-driven status change And counters display zero when empty and never show negative or null values
Visual and Audio Alerts for Key Changes
Given the dashboard is open and sound is enabled When a volunteer changes to Confirmed Then a visual banner appears and a single audio tone plays within 3 seconds, with an action to View the volunteer And when a volunteer becomes At Risk, a distinct banner and tone fire within 3 seconds And the same volunteer is not alerted more than once for the same transition within 60 seconds And the captain can mute alerts for 10 minutes from the banner, and system Do Not Disturb silences audio while preserving visual banners
Real-time Updates with WebSocket Fallback and Offline Resilience
Given a healthy network, live updates are delivered via WebSocket with median data staleness under 5 seconds When the WebSocket is unavailable, the client falls back to polling every 30 seconds until the socket reconnects And if connectivity drops, an Offline badge and last refreshed timestamp are shown; the last successful snapshot remains accessible And upon reconnection, the dashboard reconciles to current state within 10 seconds without duplicate alerts or rows And retries use exponential backoff starting at 2 seconds and capping at 60 seconds
Impact Board En-route and Arrival Metrics Integration
Given a shift with volunteers changing status When a volunteer sets On The Way or is marked Arrived Then the En Route and Arrived metrics are published to the Impact Board within 10 seconds And the Impact Board counts for the shift equal the dashboard’s corresponding counts within plus or minus 1 And if offline, metric events are queued and delivered on reconnect in timestamp order within 60 seconds

Auto Language

Sends each blitz in the volunteer’s preferred language with concise, accessible directions and map links. Improves response rates and inclusion across multilingual, SMS-first crews.

Requirements

Volunteer Language Preference Management
"As a volunteer, I want to set and keep my preferred language so that I always receive instructions I understand without extra steps."
Description

Capture and persist each volunteer’s preferred language during signup, import, or first contact, and use it to automatically select the language for all outbound blitzes, receipts, reminders, and shift messages. Provide admins with UI controls to view and edit preferences in profiles, set campaign- or crew-level defaults, and define a fallback hierarchy when a translation is unavailable. Support preference updates via SMS keyword (e.g., LANG ES) and mobile web, log changes for auditability, and ensure preferences propagate through all messaging workflows without extra organizer steps.

Acceptance Criteria
Capture and Persistence via Signup, Import, and First Contact
Given a public signup form with a language selector defaulted from the active campaign or crew, When a volunteer selects Español and submits, Then preferredLanguage = "es" is saved on the profile in ISO 639-1 lowercase and is retrievable via UI, export, and API. Given a signup submission with no language selected, When the record is created, Then preferredLanguage is set to the campaign default if present, else the crew default, else the org default. Given a CSV import mapped with a column named "language" or "preferred_language" containing values like "es", "ES", or "Español", When the import runs, Then the system normalizes and saves preferredLanguage = "es" and records source = "import" for each changed row. Given a CSV row where the language value is unsupported (e.g., "zz"), When the import runs, Then the row is rejected with a field-level error listing supported codes, and other valid rows import successfully. Given a volunteer with no preferredLanguage on first contact via SMS, When the volunteer replies with a supported keyword pattern (e.g., "LANG ES"), Then preferredLanguage is created as "es" and used for all subsequent messages.
Outbound Messaging Selection and Fallback Hierarchy
Given a volunteer with preferredLanguage = "es", When a blitz, receipt, reminder, or shift message is queued, Then the system selects the Spanish template/variant and sends the message in Spanish. Given a volunteer with preferredLanguage = "fr" and no French translation exists for the selected template, When the message is queued, Then the system applies the fallback hierarchy in order: volunteer preference translation → crew default → campaign default → org default → system default ("en"), and sends using the first available translation. Given message analytics, When delivery completes, Then the message record stores the language actually sent and the fallback step used (if any) for reporting and QA.
Admin UI: View/Edit Preference and Manage Defaults
Given an organizer with permission to edit volunteers, When they open a volunteer profile, Then they can view the current preferredLanguage and edit it via a supported-languages dropdown, with values displayed as native names plus ISO code (e.g., "Español (es)"). Given an admin on a campaign or crew settings page, When they set a default language and save, Then the default is persisted and applied to new signups lacking a selection and to fallback resolution per the defined hierarchy. Given an admin changes a volunteer’s preferredLanguage from "en" to "es", When they save, Then the change is immediately reflected in the profile, future messages use "es", and the change is recorded in the audit log.
Self-Service Update via SMS Keyword
Given a verified volunteer phone number, When the volunteer texts any of the supported patterns (e.g., "LANG ES", "LANGUAGE ES", "IDIOMA ES") case-insensitively, Then the system updates preferredLanguage = "es", replies with a confirmation in Spanish, and includes a help link. Given an invalid or unsupported code (e.g., "LANG ZZ"), When received, Then the system does not change the preference and replies with a list of supported codes and examples. Given abuse-prevention limits of 3 changes per 24 hours, When the limit is exceeded, Then the system rejects the change, replies with a throttling notice, and logs the attempt without altering the current preference.
Self-Service Update via Mobile Web
Given an authenticated volunteer session on the mobile web portal, When the volunteer opens Language Preferences and selects Français (fr) and saves, Then preferredLanguage = "fr" is persisted, a success confirmation appears in French, and subsequent outbound messages use French. Given a stale session, When the volunteer attempts to change language, Then the system prompts re-authentication and only applies the change after successful verification. Given accessibility requirements, When the language selector is rendered, Then it meets WCAG 2.1 AA (labels, focus order, ARIA where needed) and lists only supported languages.
Audit Log and Change History
Given any change to preferredLanguage (signup, import, admin edit, SMS, mobile web), When the change is committed, Then an audit event is saved with actor (user id or system), source (signup/import/admin/sms/web), timestamp (ISO 8601 UTC), volunteer id, old value, and new value. Given an organizer opens a volunteer’s history tab, When audit entries are loaded, Then they can filter by source and export the change history as CSV including all fields above. Given data retention policies, When the audit log exceeds 12 months of entries, Then entries are retained for at least 24 months or per org policy without loss of integrity.
Propagation Across Workflows and Scheduled Sends
Given a reminder and a receipt scheduled for a volunteer for later today with language currently "en", When the volunteer updates preferredLanguage to "es" before send time, Then both scheduled messages send in Spanish without any organizer action. Given an in-flight campaign with thousands of queued messages, When language preferences change for some recipients before send time, Then those messages resolve templates at send time using the latest preference and fallback, with propagation latency under 60 seconds. Given previously sent messages, When the preference changes later, Then historical message content and metadata remain unchanged while all future messages use the new preference.
Multilingual Message Templates & Variables
"As an organizer, I want to author and preview templates in multiple languages so that every volunteer receives clear, localized instructions."
Description

Enable organizers to create, manage, and preview message templates for blitzes, reminders, receipts, and shift assignments with language-specific variants. Provide placeholder variables (name, shift time, location, map link) that render correctly across languages, including right-to-left support and locale-safe punctuation. Include character/segment counters to keep SMS concise, automatic fallback to default language when a variant is missing, and side-by-side previews to validate clarity and length constraints before sending.

Acceptance Criteria
Create & Manage Language-Specific Templates
Given an organizer with template-manage permissions When they create templates for message types: Blitz, Reminder, Receipt, Shift Assignment Then they can add language variants for at least English, Spanish, and Arabic And the organizer can set one variant as the default language per template And saving a variant preserves existing variants without overwriting other languages And deleting a non-default variant succeeds without removing the template And deleting the default variant requires selecting a new default before completion And template titles are unique per message type + language combination
Variables Render with Locale & RTL Support
Given a template containing variables: {{name}}, {{shift_time}}, {{location_name}}, {{map_link}} When the organizer previews in Spanish (es) and Arabic (ar) Then {{shift_time}} is formatted using the locale’s date/time conventions for es and ar And punctuation and spacing are locale-appropriate (e.g., spacing before colon in fr; bidi-safe punctuation in ar) And Arabic previews render right-to-left with correct alignment and bidi handling for embedded numerals and URLs And {{map_link}} remains clickable and visually intact in both LTR and RTL previews And no variables render as raw tokens in preview
Template Variable Validation & Safety
Given an organizer edits a template When they include an unknown placeholder (e.g., {{nam}}) Then the editor blocks save and highlights the unknown token with a corrective suggestion When they include the supported variables only Then the template saves successfully without warnings When previewing with sample data that includes special characters (e.g., apostrophes, quotes, Arabic letters) Then variables render without broken punctuation, escaping issues, or mojibake
Accurate Character and SMS Segment Counting
Given a template variant being edited When the organizer types content in English, Spanish, and Arabic Then the character counter updates live for each variant And the SMS segment counter accurately reflects GSM-7 vs Unicode encoding rules for each variant And switching language variants recalculates segments accordingly And a warning is shown when content exceeds 1 segment and a stronger warning at 3+ segments And counters are also displayed in the side-by-side preview for each language
Default Language Fallback When Variant Missing
Given recipients have preferred languages set And a template has English (default) and Spanish but no Arabic variant When sending a batch that includes an Arabic-preference recipient Then the system automatically uses the default (English) variant for the Arabic recipient And the send log records the fallback event with recipient, intended language, and used language And the message send does not fail due to missing variant And no recipient receives a mixed-language message body
Side-by-Side Preview for Clarity and Length Validation
Given a template with at least two language variants When the organizer opens the side-by-side preview and selects a sample recipient Then previews for each selected language render simultaneously with resolved variables And LTR variants are left-aligned; RTL variants are right-aligned And {{map_link}} is a valid HTTP(S) URL and remains unbroken in all variants And character and segment counters display per variant And changing the sample recipient updates all previews and counters within 500 ms
Translation Workflow & Quality Assurance
"As a coordinator, I want translations to follow our approved terms and be reviewed when needed so that messages are accurate and culturally appropriate."
Description

Provide a translation pipeline that supports machine translation with a project-specific glossary and optional human review/approval per language. Maintain versioning and approval status for each template-language pair, highlight variable mismatches or missing translations, and run automated checks for reading level and accessibility. Allow batch translation when templates change, surface diffs for reviewers, and prevent unsent or unapproved content from being used in live blitzes. Store glossary terms (e.g., nonprofit-specific phrases) to ensure consistency across messages.

Acceptance Criteria
Glossary Management and Machine Translation Application
Given a template with variables and a project glossary containing source–target pairs per language When I run machine translation for Spanish Then glossary terms are applied exactly as defined for Spanish in 100% of occurrences And all placeholders (e.g., {{first_name}}, {map_url}) are preserved verbatim and unaltered And the translation is saved as a new draft version with status "Unapproved" for that template–language pair And deviations from glossary preferences in the draft are flagged as warnings for reviewer attention And glossary supports create/update/delete and CSV import/export with validation of duplicates and missing targets
Per-Language Human Review and Approval
Given a draft translation for template X in Spanish exists When a reviewer with Spanish permissions opens the draft Then the UI shows source vs. target side-by-side with diffs from the prior version And the reviewer can Approve or Request Changes with a required comment And upon approval the status becomes "Approved", capturing reviewer ID and timestamp And the newly approved version becomes the default used for Spanish for template X
Versioning and Audit Trail per Template–Language
Given an approved Spanish version v1 exists When a new draft is saved Then the system increments the version number for that template–language pair (e.g., v2) And all versions retain immutable content, status, reviewer, and timestamps in an audit log And a user can restore a prior version, creating a new draft with content from that version And the UI/API expose per-language status values: Not Translated, Unapproved, Approved, and Archived And the current approved version is clearly indicated in the UI and API
Placeholder and Variable Validation
Given the source template contains the set of placeholders { {{first_name}}, {{shift_time}}, {map_url} } When a translation draft is saved for any language Then the system verifies the target contains the exact same set of placeholders, no more and no less And if mismatches are found, saving remains allowed but approval is blocked with explicit errors listing missing or extra placeholders And any unbound variables or malformed tokens are flagged and must be corrected before approval
Automated Readability and Accessibility Checks
Given a project threshold of Grade 6 reading level and SMS accessibility rules are configured When a translation draft is saved Then the system calculates reading grade for the target text and flags if above threshold And flags all-caps words longer than 3 characters, sentences longer than 25 words, and ambiguous link text (e.g., "click here") And approval requires acknowledgment of warnings or remediation; hard errors configured as "block" prevent approval And all check results are stored with the draft and visible to reviewers
Batch Translation and Diff Review After Template Change
Given an approved English template is edited When I trigger Batch Translate for selected languages (e.g., Spanish, Hindi) Then only changed segments are machine translated using the glossary and saved as drafts per language And reviewers see per-language diffs highlighting added, removed, and modified text And the system tracks batch job progress and reports counts of drafts created, approvals pending, and errors
Prevent Unapproved Content in Live Blitzes
Given a blitz is scheduled and volunteers have preferred languages When the system assembles outgoing messages Then it uses only the latest Approved version of each template–language pair And if a volunteer’s language lacks an approved version, the system blocks sending to that volunteer and surfaces a blocking error listing missing languages And no Unapproved or Draft content is sent via any channel or API
Inbound Language Detection & Auto-Set Preference
"As a volunteer, I want the system to recognize my language from my first interaction so that I don’t have to change settings manually."
Description

Detect a volunteer’s likely language from initial signup inputs, device locale on mobile web, or the language of the first SMS reply, and automatically set or suggest a preferred language with confidence thresholds. Avoid unwanted flips by requiring confirmation or admin review when confidence is low, and notify organizers of bulk shifts in detected language for a campaign. Log detection events and allow easy correction via profile edit or SMS keyword.

Acceptance Criteria
Auto-set from High-Confidence First SMS Language
Given a new volunteer's first SMS reply is received And the language detector returns language L with confidence >= 0.85 And the volunteer has no Preferred Language set When the message is processed Then set Preferred Language to L And use L for all subsequent outbound messages to that volunteer And create an audit log entry with source=SMS, detected_language=L, confidence, previous_language=null, new_language=L, action=auto_set
Confirmation Flow for Low/Mid Confidence Detection
Given a new volunteer submits a mobile web signup or sends a first SMS reply And the language detector returns language L with confidence between 0.50 and 0.84 inclusive And the volunteer has no Preferred Language set When the event is processed Then send a bilingual confirmation prompt (English + L) with options: reply 1 to confirm L, reply 2 for English And do not set Preferred Language until a confirmation is received And if no response within 24 hours, mark status=pending and add the volunteer to the admin review queue And create an audit log entry with action=prompted_for_confirmation
Web Signup: Device Locale and Text Input Signals
Given a volunteer opens the mobile web signup page And the system detects language L from either (a) browser/device Accept-Language or (b) free-text signup inputs with detector confidence >= 0.85 When the volunteer submits the form Then render the UI in L during the session And if the volunteer has no Preferred Language set, set Preferred Language to L And create an audit log entry with source=device_locale or source=signup_input as applicable, detected_language=L, confidence, previous_language=null, new_language=L, action=auto_set
Guardrails Against Unwanted Language Flips
Given a volunteer already has Preferred Language P And an inbound message is detected as language L where L != P with confidence C When the message is processed Then apply these rules: - If C < 0.95, do not change Preferred Language; send a one-time confirmation prompt to switch to L - If C >= 0.95 on two consecutive inbound messages within 7 days, auto-change Preferred Language to L - Do not auto-change Preferred Language more than once within 30 days; require explicit confirmation or admin approval for additional changes And create an audit log entry describing the rule applied and action taken
Organizer Alert for Bulk Language Shifts
Given a campaign has ongoing detections across volunteers When, within any rolling 24-hour window, detected language L accounts for >= 30% of new detections and increases by >= 15 percentage points versus the prior 7-day average Then send an organizer alert via in-app notification and email summarizing counts by language, confidence distribution, and affected segments And include links to the review queue and bulk messaging templates in language L And log the notification event with campaign_id, time_window, language=L, metrics, action=organizer_alert_sent
Audit Logging and Export of Detection Events
Given any language detection, confirmation prompt, user/admin change, or auto-change occurs When the event is processed Then create an immutable audit entry capturing volunteer_id, campaign_id, timestamp, source (SMS|device_locale|signup_input), detected_language, confidence, previous_language, new_language, action, actor (system|user|admin), and correlation_id And allow admins to filter events by date range, campaign, source, language, and action in the admin UI And allow export of filtered events to CSV including all fields And retain logs for at least 24 months
Manual Correction via Profile Edit or SMS Keyword
Given an admin edits a volunteer's Preferred Language in the admin UI When the change is saved Then update the volunteer's Preferred Language immediately for all subsequent communications And create an audit log entry with action=admin_override Given a volunteer sends an SMS keyword specifying a supported language (e.g., "LANG ES" or the language name) When the message is received Then validate the requested language against supported languages And update Preferred Language accordingly And send a confirmation SMS in the new language And create an audit log entry with action=user_keyword_change
Locale-aware Links, Maps, and Formatting
"As a volunteer, I want directions and event details formatted for my language and region so that I can navigate easily and arrive on time."
Description

Localize embedded links and content for each language, including map links with locale parameters, turn-by-turn directions, and address formatting rules. Format dates, times, numbers, and measurement units per locale, and ensure right-to-left layout where applicable. Use short links that preserve locale settings and remain readable in SMS, verify that links open in common local map apps, and provide graceful fallback if a localized service is unavailable.

Acceptance Criteria
Localized Map Link with Turn-by-Turn Directions
Given a volunteer has a preferred locale set (e.g., fr-FR, es-MX, ar-EG) and a blitz location with address and coordinates When an SMS blitz message is generated and sent Then the included map link contains an explicit locale parameter matching the volunteer’s locale And the directions page renders UI text in the volunteer’s locale And turn-by-turn directions display measurement units appropriate to the locale (e.g., km for fr-FR, mi for en-US) And the link resolves to a directions view within 2 seconds at the 95th percentile from target regions
Map App Compatibility by Region
Given a volunteer receives a blitz SMS with a directions link on a device with one or more map apps installed When the volunteer taps the link on iOS 17+ or Android 12+ Then the link opens in the device’s default map app if that app supports the deep link for directions And if the default app does not support the deep link, the link opens a universal HTTPS map URL in the system browser And this behavior is verified on iOS (Apple Maps, Google Maps installed) and Android (Google Maps, Waze installed) across en-US, fr-FR, and ar-EG locales
Locale-Specific Address Formatting in SMS
Given a volunteer’s locale and a normalized address (with street, unit, city, region, postal code, country) When the address is rendered in the SMS body Then the address is formatted per CLDR/libaddressinput rules for that locale (ordering, separators, postal code placement) And non-ASCII characters and diacritics are preserved And the formatted address fits within 120 characters; if longer, locale-appropriate abbreviations are applied without omitting street number, locality, or postal code And formatting is verified with test cases for en-US, fr-FR, ja-JP, and hi-IN
Date, Time, Number, and Unit Localization
Given a volunteer’s locale and an event with scheduled dates/times and distance or quantity values When the SMS text is generated Then dates and times use locale-appropriate order and clock format (e.g., en-US 8/9 2:30 PM; fr-FR 09/08 14:30) And numbers use locale-appropriate digit grouping and decimal separators (e.g., en-US 1,234.5; fr-FR 1 234,5) And measurement units reflect locale conventions (e.g., miles for en-US, kilometers for fr-FR) with correct rounding to 1 decimal place And time zone is displayed relative to the event’s local time with UTC offset when the volunteer’s device time zone differs
Right-to-Left Layout Support in Messages
Given the volunteer’s locale is right-to-left (e.g., ar, he, fa, ur) When the SMS content is composed Then the message text direction is RTL with correct punctuation mirroring And numerals are formatted per locale preference (e.g., Arabic-Indic digits for ar-EG per CLDR) And URLs are wrapped with Unicode directional marks to maintain left-to-right rendering of the link and prevent text reordering And no line exceeds 160 GSM-7 characters or 70 UCS-2 characters; if exceeded, content is shortened without breaking the URL or address
Short Links Preserve and Propagate Locale
Given a shortened URL is required for the SMS When the short link is generated Then the short link length is ≤ 24 characters and uses the GiveCrew domain And the short link encodes the target locale explicitly (e.g., as a path or query token) and resolves with 301/302 redirects that preserve the locale to the final destination And opening the short link from a browser with neutral Accept-Language still renders the destination in the intended locale And the same short link, when reshared, consistently resolves to the intended locale regardless of the opener’s device settings
Graceful Fallback for Unavailable Localized Services
Given the preferred map provider or locale-specific view is unavailable for the volunteer’s locale or device When the volunteer taps the directions link Then a web-based map opens with the closest supported language to the volunteer’s locale, else defaults to English, with the same coordinates and route prefilled And the SMS includes a concise, locale-appropriate generic instruction (e.g., “Open map for directions”) when deep linking is bypassed And a telemetry event “localization_fallback” is recorded with locale, device OS, and provider details for monitoring
SMS Encoding and Delivery Optimization
"As an organizer, I want multilingual SMS to deliver reliably without unexpected splitting or loss of meaning so that volunteers receive complete instructions."
Description

Optimize outbound messages per language by detecting GSM-7 vs UCS-2 encoding, estimating segment counts, and warning when content exceeds thresholds. Offer safe character substitutions where appropriate, preserve critical diacritics when meaning would be altered, and prevent variable expansion from causing unexpected truncation. Track delivery and concatenation behavior by carrier, adapt sending to minimize splitting, and ensure opt-in/opt-out keywords function in each supported language.

Acceptance Criteria
Encoding Detection and Segment Estimation Alerts
Given a composed message containing only GSM-7 characters When the user opens the send preview Then the system marks encoding as GSM-7 and displays the correct segment estimate (e.g., 160 chars = 1 segment; 161–306 chars = 2 segments; 307–459 chars = 3 segments) Given a composed message containing any non-GSM-7 character (e.g., emoji or accented letters not in GSM-7) When the user opens the send preview Then the system marks encoding as UCS-2 and displays the correct segment estimate (e.g., 70 chars = 1 segment; 71–134 chars = 2 segments; 135–201 chars = 3 segments) Given a message whose estimated segments exceed the default threshold of 2 segments When the estimate is calculated Then the system shows a non-blocking warning banner and highlights the characters that drive UCS-2 encoding or extra segments Given the user edits message content When characters are added, removed, or replaced Then the segment estimate and encoding indicator update immediately and accurately
Safe Substitutions With Diacritic Preservation
Given message text includes characters with safe GSM-7 equivalents (e.g., smart quotes, long dashes, non-breaking spaces) When the user opens the preview Then the system proposes safe substitutions that reduce segment count while preserving readability and shows a before/after diff with updated segment estimates Given words requiring critical diacritics in the selected language (e.g., “año”, “São”, “garçon”) When generating substitution suggestions Then the system does not propose substitutions that remove or alter those diacritics by default Given the user accepts suggested substitutions When the blitz is sent Then the outbound message uses the substituted characters, and the recorded encoding and segment counts reflect the change Given a proposed substitution would increase segments or switch encoding to UCS-2 When evaluating the suggestion Then the system suppresses the suggestion
Variable Expansion Length Safeguards
Given a template message contains variables (e.g., {{first_name}}, {{location_name}}) When the user selects recipients Then the system computes min/median/max expansion lengths from actual recipient data and shows estimated segments for worst-case expansion Given the worst-case expansion exceeds the org-configured max segments (default 4) When validating the blitz Then the system blocks sending without truncating content and displays guidance to reduce length or adjust content Given a variable value introduces non-GSM-7 characters When performing final validation prior to send Then the system reclassifies encoding, re-estimates segments, and re-applies threshold checks before allowing send Given the user edits the template to bring worst-case at or below the max segments When revalidating Then the system allows sending and logs the resolved worst-case estimate
Carrier-Level Delivery and Concatenation Tracking
Given an outbound message is sent When delivery receipts and provider metadata are returned Then the system records per message: encoding type, intended segment count, carrier MCC/MNC, country, delivery status/timestamp, and billed segments when available Given a multi-segment message is sent When assembling provider requests Then the system stores concatenation identifiers (UDH reference and sequence numbers) for audit and troubleshooting Given a user opens carrier analytics When filtering by carrier and time window (last 30 days) Then the dashboard displays delivery rate, average billed segments, and sample size for that carrier Given QA devices for top carriers are enrolled When a 3-segment GSM-7 and a 3-segment UCS-2 test are sent Then device checks confirm single-message reassembly on receipt and the result is logged per carrier
Adaptive Sending To Reduce Splitting
Given historical data shows for a carrier-country combination that average estimated segments > 2 and safe optimizations are available When preparing new sends to that combination Then the system automatically applies safe substitutions and link shortening (respecting language diacritic protections) unless the blitz disables adaptation Given adaptation is applied When comparing pre- and post-adaptation estimates Then the estimated segment count is reduced for at least 80% of adapted messages and the change is logged with rule name and before/after counts Given adaptation is applied to a message When reviewing the send log Then the log shows which optimizations were used and provides a one-click revert option for future sends in that blitz
Multilingual Opt-In/Opt-Out Keyword Support
Given supported languages are enabled and a per-language keyword list is configured (unsubscribe, resubscribe, help) When a contact replies with a keyword that matches the list (case-insensitive, diacritics-insensitive, allowing whitespace and punctuation) Then the system executes the action (unsubscribe/resubscribe/help) and sends confirmation in the contact’s preferred language Given a reply contains a keyword as part of a longer sentence When processing the inbound message Then no automated action is taken and the message is routed to the inbox for review Given a contact is unsubscribed When attempting to include them in any future blitz Then the system suppresses the send, records the suppression reason as “opted-out”, and shows the count in the pre-send summary Given an admin opens compliance logs When filtering by keyword actions Then entries display normalized keyword, detected language, action taken, timestamp, and related message ID

Captain Console

A mobile command screen showing open slots, incoming claims, ETAs, and map pins, plus Boost Radius and Resend Wave controls. Gives Captains instant control and clarity to stabilize turnout within minutes.

Requirements

Real-time Turnout Feed
"As a Captain, I want a real-time view of open and at-risk slots so that I can focus my efforts where they will most quickly stabilize turnout."
Description

Provides an always-on, auto-refreshing feed of open, claimed, and filled slots for today and upcoming shifts, with badges for risk (e.g., low fill, late arrivals) and progress. Supports fast filters by event, role, location, and time window, plus sorting by start time or urgency. Uses push updates (WebSockets/SSE) with polling fallback to keep Captains current even on flaky mobile networks. Integrates with Events, Assignments, and Claims services to reflect changes within seconds, and maintains a lightweight offline cache for continuity. Delivers instant situational awareness so Captains can prioritize actions and stabilize turnout quickly.

Acceptance Criteria
Live Push Updates with Polling Fallback
Given the device has connectivity and an active session When a slot is claimed, unclaimed, created, or filled in the backend Then the feed reflects the change within 5 seconds via WebSockets/SSE Given the WebSocket/SSE connection drops When connectivity is present Then the client falls back to polling every 15 seconds until push reconnects Given the same update arrives via push and poll Then the feed de-duplicates events and shows each change only once Given the client is connected Then a connection status indicator shows "Live" for push, "Polling" for fallback, and "Offline" when no connectivity
Auto-Refreshing Feed for Today and Upcoming
Given the Captain opens Captain Console When the feed loads Then it displays shifts from now through the next 48 hours by default Given shifts have slots Then each shift row shows counts for open, claimed, and filled slots and a progress bar Given new shifts are scheduled or canceled Then the feed inserts or removes them within 10 seconds without manual refresh Given the app is kept open Then the feed auto-refreshes continuously without user action
Risk and Progress Badges
Given a shift starts within 2 hours and fill rate is below 70% Then a "Low Fill" badge appears on the shift Given at least 2 claimed volunteers have ETAs more than 10 minutes past start Then a "Late Arrivals" badge appears on the shift Given fill rate reaches 100% Then a "Filled" badge replaces "Low Fill" and risk badges clear Given ETAs or claims change Then badges update within 5 seconds
Fast Filters and Sorting
Given the Captain selects filters for event, role, location, and time window When Apply is tapped Then the feed reflects the filters within 300 ms on a mid-tier mobile device Given Start Time sort is chosen Then items are ordered ascending by start time; ordering is stable for equal times Given Urgency sort is chosen Then items are ordered by risk severity first (Low Fill and Late Arrivals before others), then by proximity to start (sooner first), then by lower fill percentage; ties are stable Given the app is relaunched within 24 hours Then the last-used filters and sort are restored
Offline Cache and Continuity
Given network connectivity is lost Then an "Offline" banner appears and the feed remains browsable from a cache no older than 30 minutes Given the user applies filters or sorting while offline Then they operate on cached data locally without errors Given connectivity is restored Then the feed syncs delta updates and reflects current state within 10 seconds without duplicating or dropping items Given the local cache exceeds 10 MB Then the oldest records are pruned while preserving the last 24 hours of shifts when possible
Resilience on Flaky Networks
Given connectivity flaps online/offline up to 5 times within 2 minutes Then the client uses exponential backoff for push reconnect attempts capped at 60 seconds and maintains polling at 15-second intervals when online Given reconnection succeeds Then the feed reconciles missed updates using a since-last-seen cursor and displays a consistent state within 30 seconds Given repeated connection errors while connected Then keep-alive pings are limited to at most 1 per minute
Service Integration Accuracy
Given changes occur in Events, Assignments, or Claims services When a volunteer claim is created, updated, or cancelled Then the feed shows the corresponding shift's slot counts and status correctly within 5 seconds Given conflicting updates arrive out of order Then the client resolves using server timestamps to avoid state regression and duplicate entries Given a service returns an error Then the feed logs the incident, shows a non-blocking error toast, and retries with exponential backoff without freezing the UI
Claim Triage & Auto-Conflict Check
"As a Captain, I want to quickly review and confirm incoming claims with automatic conflict checks so that I can fill slots fast without creating assignment errors."
Description

Introduces a mobile triage queue for incoming volunteer claims with one-tap actions to Accept, Waitlist, or Reject. Automatically checks for conflicts (overlapping shifts, duplicate roles, capacity limits, missing prerequisites) before confirmation and displays clear reasons with suggested resolutions. Applies a short hold on a slot while reviewing to prevent double-booking, and triggers the correct message template (acceptance, waitlist, or decline) on action. Logs all decisions to the activity audit and updates the Assignments service atomically to avoid race conditions. Reduces errors and speeds decisions so Captains can convert more claims with confidence.

Acceptance Criteria
One-Tap Accept for Conflict-Free Claim
Given a new volunteer claim appears in the triage queue and no conflicts are detected (overlap, duplicate role, capacity, prerequisites) at time of review And a review hold is active on the requested slot When the Captain taps Accept Then the system revalidates conflicts within the same transaction And atomically writes the assignment to the Assignments service And releases the hold And removes the claim from the triage queue and marks it Accepted And sends the acceptance message template to the volunteer And writes an activity audit entry for the decision And the Captain sees a success confirmation within 3 seconds
Auto-Conflict Check: Overlapping Shift or Duplicate Role
Given the volunteer has an existing assignment that either overlaps the requested shift by any minute or matches the same role for the same event When the Captain opens the claim in triage Then a conflict banner lists the specific reason ("Overlapping shift" or "Duplicate role") with the conflicting shift/role details And the Accept action is disabled until the conflict is resolved And suggested resolutions include moving to the next non-overlapping slot, selecting a different role, or Waitlist When the Captain selects a non-overlapping slot or different role Then the conflict clears and Accept becomes enabled When the Captain attempts to Accept without resolving Then the system prevents the commit and surfaces the conflict inline with no assignment created
Auto-Conflict: Capacity Limit or Missing Prerequisites
Given the role’s capacity for the requested shift is already full or the volunteer is missing required prerequisites (e.g., training/certifications) When the claim is opened in triage Then a conflict banner lists the applicable reason ("Capacity reached" with current count/max or the specific missing prerequisite names) And Accept is disabled And available actions include Waitlist with a prefilled reason and, for prerequisites, "Request prerequisite" to notify the volunteer When the Captain taps Waitlist Then the claim moves to Waitlist state, the hold is released, the waitlist message is sent, and an audit entry is written When prerequisites are later marked satisfied for this claim Then the conflict clears and Accept becomes enabled
Short Hold Prevents Double-Booking During Review
Given a claim enters review in the triage queue When the Captain opens the claim Then the system places a visible hold on the requested slot for 120 seconds And other users attempting to Accept into the same slot see "Held by [Captain]" and cannot Accept while the hold is active When the Captain takes any action (Accept, Waitlist, Reject) Then the hold is released immediately When 120 seconds elapse with no action Then the hold auto-releases and the claim returns to its prior queue state And all hold start, release, and expiration events are written to the activity audit
Message Templates Triggered by Triage Action
Given acceptance, waitlist, and decline message templates are configured with tokens {first_name}, {role}, {shift_time}, {location}, {org} When the Captain taps Accept Then the acceptance template is dispatched to the volunteer’s preferred channel(s) within 10 seconds with all tokens resolved When the Captain taps Waitlist Then the waitlist template is dispatched within 10 seconds with the waitlist reason included When the Captain taps Reject Then the decline template is dispatched within 10 seconds with the decline reason included If any dispatch fails Then the UI shows a non-blocking error with retry and the failure is captured in the audit with a provider message ID
Atomic Assignment Update Under Concurrency
Given two Captains attempt to Accept the same claim or slot within overlapping time windows When both tap Accept Then exactly one transaction commits the assignment and the other receives "Slot already filled" within 2 seconds And no duplicate assignments are created And repeated taps by the same Captain do not create duplicates (idempotent Accept) And the triage queue updates in near real time for both Captains
Activity Audit Logs for Triage Decisions
Given any triage action occurs When the action is completed Then an audit entry is written capturing: claim_id, volunteer_id, role_id, shift_id, action, actor_user_id, timestamp (UTC), pre-action conflict list, post-action state, message_template_id (if any), delivery status/result, and hold events And audit entries are immutable and queryable by claim_id and volunteer_id And the audit entry is available to query within 2 seconds of the action
ETA Tracking & Map Pins
"As a Captain, I want to see who is on the way and who is running late so that I can proactively cover gaps before they impact turnout."
Description

Displays live ETAs and location pins for confirmed volunteers who opt in, with color-coded status (on time, tight, late, unknown) and pin clustering for dense areas. Supports low-bandwidth map tiles and updates via lightweight pings or self-reported ETAs through a secure link; gracefully degrades to last-known location or ETA when live data is unavailable. Protects privacy with opt-in, time-bound visibility, and coarse location jitter as configured. Integrates with the Volunteer profile and Event schedule to compute risk scores and surfaces alerts for late arrivals. Helps Captains anticipate gaps and decide when to trigger Boost or Resend actions.

Acceptance Criteria
Volunteer Opt-In, Time-Bound Visibility, and Privacy Protections
- Given a volunteer opens a secure, single-use tracking link bound to an event and shift, When they tap “Share ETA/Location,” Then their pin and ETA appear to Captains only during the visibility window (default: from 90 minutes before to 30 minutes after shift start; configurable). - Given a volunteer toggles opt-out or the visibility window expires, When the server records the change, Then the pin and ETA are removed from all Captain Consoles within 30 seconds and further updates from that token are rejected. - Given coarse location jitter is configured to 150 meters (configurable 50–300 m), When a pin is rendered, Then the displayed position is randomized within the jitter radius and exact coordinates are never shown in any UI or export. - Given a volunteer has not opted in, When the Captain Console loads, Then no pin or ETA is shown for that volunteer and no background tracking occurs. - Given a token is opened after expiration, When the link is accessed, Then an expiration message is shown and no location/ETA is accepted.
ETA Calculation and Status Color Coding
- Given shift start time S and computed ETA E, When E <= S, Then status is On Time (green). - Given E > S and (E − S) <= G where G is the grace window from Event Settings (default 5 minutes), Then status is Tight (amber). - Given (E − S) > G, Then status is Late (red). - Given no valid update is received for U minutes (default 3 minutes) and no self-reported ETA exists within U minutes, Then status is Unknown (gray) and the last-known “as of <timestamp>” is displayed. - Given any new update (ping or self-ETA), When processed, Then ETA and status recalculate within 5 seconds and the UI reflects the change within 2 seconds.
Map Pin Rendering, Clustering, and Low-Bandwidth Mode
- Given network latency > 1500 ms or bandwidth < 200 kbps is detected, When the map loads, Then low-bandwidth tiles are used (each tile <= 50 KB) and map animations are disabled. - Given > 5 pins fall within a 100-meter radius at the current zoom, When rendering, Then they are shown as a cluster with a count badge; tapping a cluster zooms/expands to reveal individual pins. - Given the map cannot fetch tiles, When pins are available, Then last-known pins render over a minimal grid and a “Low connectivity” banner is shown with a list fallback of volunteers and ETAs. - Given up to 200 concurrent pins, When panning or zooming, Then visual updates complete within 500 ms for 95% of interactions in low-bandwidth mode.
Hybrid Updates via Passive Pings and Secure Self-Reported ETAs
- Given the mobile app is installed and opt-in is active, When the volunteer is en route, Then the app sends lightweight pings (<= 1 KB) every 60s ±15s and the server timestamps and stores them within 2 seconds. - Given the volunteer does not have the app, When they submit an ETA via the secure link form, Then the ETA is accepted without login and applied to status calculations immediately. - Given both ping and self-reported ETA updates exist, When determining the display value, Then the most recent timestamp prevails and a source indicator (device or self-report) is shown. - Given no updates are received for > 5 minutes, When rendering, Then status becomes Unknown and the last-known ETA/location is displayed with an “as of” timestamp.
Risk Scoring and Captain Alerts for Late Arrivals
- Given volunteer reliability R (0–100) from the profile and ETA deviation D = E − S, When computing risk, Then a score 0–100 is produced using configured weights (default: 60% D scaled to grace, 40% (100 − R)) and recomputed on each update. - Given risk >= T (default 70) or status = Late, When evaluated, Then a Late Risk alert appears in Captain Console within 5 seconds, highlighting the row and adding a bell icon. - Given an alert was shown for a volunteer in the last 10 minutes, When risk remains >= T, Then duplicate alerts are suppressed and the existing alert persists until risk < T or the volunteer is marked Arrived. - Given an alert is open, When the Captain taps it, Then quick actions for Resend Wave and Boost Radius are presented with prefilled defaults based on the gap size.
Graceful Degradation and Post-Event Data Handling
- Given live data is unavailable, When rendering the console, Then the last-known ETA and pin are shown with an “as of” timestamp and no new location attempts are made after 10 minutes of inactivity. - Given connectivity is restored, When fresh data arrives, Then pins and statuses refresh within 10 seconds and an "Updated" toast confirms the change. - Given the event has ended, When retention policies run, Then raw coordinates are purged and only status changes and timestamps are retained; no pins are visible outside the event window.
Boost Radius Control with Fill Forecast
"As a Captain, I want to widen the outreach radius with a clear forecast so that I can quickly pull in nearby volunteers to fill urgent openings."
Description

Adds a slider and presets to expand the recruitment radius around an event, with targeting by role and shift window. Displays an estimated candidate pool and projected fill probability based on contact density and historical conversion. On activation, triggers geotargeted outreach sequences to nearby opted-in volunteers via SMS/push, respecting rate limits, quiet hours, and consent. Auto-expires after a set duration or when targets are met, and records outcomes to the activity log and forecasting model. Gives Captains a fast, data-informed lever to attract nearby help within minutes.

Acceptance Criteria
Adjust Radius, Role, and Shift Window Controls Render and Validate
- Given I am a Captain on the Captain Console for a geocoded event, When I open the Boost Radius control, Then I can set radius via a slider with visible min/max and select from predefined presets. - Given roles exist for the event, When I open the role selector, Then I can multi-select one or more roles and see the current selection reflected in the summary chip. - Given the event has defined shifts, When I open the shift window selector, Then I can choose a specific shift or a time window that aligns to the event’s time zone. - Given any required input is missing or invalid (e.g., no role selected when required, shift window outside event time), When I attempt to activate, Then the Activate action is disabled and an inline validation message indicates what to fix. - Given I adjust radius via slider or preset, When I release the control, Then the selected radius value persists and is used for subsequent calculations.
Real-time Candidate Pool and Fill Probability Forecast Display
- Given valid radius, role(s), and shift window are selected, When any of these inputs change, Then the estimated candidate pool and projected fill probability update within 2 seconds. - Given there are eligible contacts within the radius that match role and shift criteria, When the forecast renders, Then it displays a numeric candidate count and a percentage fill probability. - Given there are zero eligible contacts, When the forecast renders, Then it displays 0 candidates, 0% projected fill, and the Activate action is disabled. - Given the forecast is computed, When I view the forecast, Then it indicates the reference basis (contact density and historical conversion) and the timestamp of calculation. - Given the same inputs and underlying data, When I re-open the Boost dialog within the same session, Then the forecast values are consistent (±0 difference).
Boost Activation Triggers Consent-Respecting Geotargeted Outreach
- Given a valid forecast is present and activation is enabled, When I tap Activate and confirm, Then the system enqueues outreach only to contacts within the selected radius who match role/shift filters and have current consent for the chosen channels (SMS and/or push). - Given activation succeeds, When the queue is created, Then the UI shows a success confirmation with counts of intended recipients by channel and a Boost status indicator changes to Active. - Given a contact qualifies for multiple channels, When messages are queued, Then deduplication ensures the contact receives at most one initial outreach per wave as configured. - Given activation occurs, When I view the event metrics, Then the open slots count and “Boost active” badge reflect the running boost within 10 seconds.
Quiet Hours and Rate Limits Enforcement with Scheduling
- Given org quiet hours are in effect for a contact’s locale, When a boost is activated, Then no message is sent to that contact immediately and the outreach is scheduled for the next permissible window with the scheduled time shown in the UI. - Given provider or org rate limits, When the boost would exceed throughput, Then sending is throttled to the allowed rate and the UI displays an estimated completion time. - Given a per-contact frequency cap is configured, When a contact has reached the cap, Then the contact is excluded from the recipient list and the exclusion is logged. - Given enforcement actions occur (quiet hours, throttling, frequency caps), When I open the boost details, Then I can see counts for sent, scheduled, throttled, and excluded recipients by reason.
Auto-Expiry and Target-Based Early Stop of Boost
- Given a boost is active, When the configured boost duration elapses, Then the boost auto-expires and all unsent messages are canceled. - Given a boost is active with a target number of open slots, When the target is met (or the fill probability exceeds a configured threshold), Then the boost stops early and no further messages are sent. - Given a boost is running, When I manually stop it, Then the system halts pending outreach immediately and marks the boost as Stopped by user with timestamp. - Given any stop condition occurs, When I view the Boost status, Then I see the final state (Expired, Auto-stopped, or Stopped) and a summary of outcomes.
Activity Log, Attribution, and Forecast Model Feedback
- Given a boost session begins, When it is created, Then an activity log entry records captain ID, timestamp, event ID, radius, roles, shift window, and forecast values. - Given outreach proceeds, When messages are sent and responses occur, Then the activity log aggregates counts for reached, replies, claims, signups, and conversions linked to the boost session. - Given the boost ends, When final outcomes are available, Then the forecasting dataset is updated with inputs and outcomes for model retraining/audit, including model version used. - Given logging is complete, When I open the event’s activity log, Then I can filter by Boost actions and export the boost report as CSV.
Failure Handling and Guardrails for Missing Data or Invalid Config
- Given the event lacks a valid location or geocoding fails, When I open the Boost Radius control, Then the control is disabled and an error message explains that a location is required. - Given messaging providers return an error, When sending fails for a subset of recipients, Then the system retries with exponential backoff up to a configured limit and logs failures with reasons. - Given inputs exceed allowed limits (e.g., radius beyond max), When I attempt to apply them, Then the value is clamped to the maximum and a validation notice is shown. - Given a data permission or consent mismatch is detected, When building the recipient list, Then affected contacts are excluded and the exclusion reason is recorded in the boost details.
Resend Wave Orchestrator
"As a Captain, I want to schedule targeted reminder waves so that more volunteers confirm and show up without me sending manual messages."
Description

Enables one-tap setup of reminder waves to unconfirmed or at-risk volunteers with schedule controls (send now, in X minutes, or at set times) and channel mix (SMS, email, push). Provides a template library with merge fields and compliance safeguards (opt-out respect, 10DLC throttling, quiet hours). Deduplicates contacts across waves, supports A/B variants for copy and timing, and reports delivery, response, and conversion to filled slots. Integrates with Messaging and Assignments services to update statuses in real time. Increases confirmations and reduces no-shows with minimal Captain effort.

Acceptance Criteria
Send Now Wave to Unconfirmed Volunteers
Given a Captain is viewing the Captain Console for an event with open slots and unconfirmed/at-risk assignees When the Captain selects Resend Wave, chooses Send Now, and selects channels SMS, Email, and Push Then the system builds the recipient list as all unconfirmed or at-risk assignees who are opted-in to at least one selected channel and have not been contacted by any active or sent wave for this event in the past 24 hours And the list is deduplicated so each person appears once across all selected channels And the wave is enqueued immediately and first sends start within 10 seconds of launch And recipients lacking any reachable selected channel are excluded and counted under No Reachable Channel And messages render with correct merge fields (first_name, shift_time, location_name, confirmation_link, opt_out_footer) per recipient And Messaging and Assignments services update statuses in real time; delivery, bounce, reply, and confirmation events reflect on the Captain Console within 5 seconds
Schedule Wave in X Minutes
Given the Captain selects Resend Wave and chooses In X Minutes with X between 1 and 120 When the Captain saves the configuration Then a scheduled job is created to release the wave at now + X minutes And the Captain can cancel the wave before its release; canceled waves send zero messages And the Captain can edit template, channels, and audience filters before release; edits apply to the release build at send time And at release, per-recipient sends respect quiet hours; any recipient in quiet hours is deferred to the next allowed window and marked Deferred - Quiet Hours And all deferrals, cancellations, and edits are logged with timestamp and actor
Schedule Wave at Set Times
Given the Captain selects one or more absolute send times in the event’s timezone When the wave is saved Then the system validates all times are in the future and not more than 30 days out And the system creates releases at each set time And per-recipient quiet hours are enforced; recipients in quiet hours at a release are deferred to the next allowed window And deduplication across releases is enforced so no recipient receives more than one send from the same wave
Channel Mix Selection and Fallbacks
Given the Captain selects a channel mix and an org-level channel preference order exists When the wave is built Then each recipient is assigned exactly one send via their highest-ranked available channel unless Multi-channel is explicitly enabled And if Multi-channel is enabled, subsequent channels are only sent if no reply/confirmation is recorded within the configured delay (default 10 minutes) And Push is only sent to recipients with an active device token and notifications enabled; Email is suppressed for recipients with a recent hard bounce; SMS is suppressed for invalid or blocked numbers And all suppressions and channel choices are auditable per recipient
Template Library and Merge Fields Validation
Given the Captain selects a message template from the library When the wave configuration is saved or launched Then all required merge fields in the template resolve for each recipient or use configured fallbacks; recipients missing required data without a fallback are excluded with reason Missing Data And the preview renders sample messages for at least 3 recipients per selected channel And confirmation, decline, and reschedule links are signed, unique per assignment, and expire per org policy And SMS templates include opt-out language where required; Email templates include an unsubscribe link and physical address footer And the Captain cannot launch the wave until template validation passes
Compliance Safeguards (Opt-Out, Quiet Hours, 10DLC)
Given org-level quiet hours and carrier compliance settings are configured When a wave is executed Then recipients who have opted out of a channel do not receive messages on that channel and are labeled Opted Out And SMS sending respects 10DLC throughput and daily cap limits; messages are rate-limited and queued to prevent carrier violations, with no more than the configured TPS per brand/number And STOP, UNSUBSCRIBE, or QUIT SMS replies immediately update opt-out status and halt any pending sends to that recipient within 5 seconds And emails include compliant headers and are not sent to addresses on the suppression list; hard bounces update suppression immediately And all compliance actions are recorded in an immutable audit log
A/B Variants and Conversion Reporting
Given the Captain creates two or more variants differing by copy and/or scheduled time with a defined split (e.g., 50/50) When the wave launches Then recipients are randomly and evenly assigned to variants according to the split, stratified by assignment type if configured And no recipient is assigned to more than one variant And the system tracks per-variant metrics: delivered, unique link clicks, replies with affirmative intent, confirmations (slot filled), and time-to-confirm within a configurable measurement window And the Captain can select a winning variant based on confirmations and send it to remaining eligible, unsent recipients with one tap And Assignments update to Confirmed within 10 seconds of a successful confirmation action, removing those recipients from future waves
Captain Alerts & Thresholds
"As a Captain, I want timely alerts with clear next steps when turnout is at risk so that I can act immediately to keep the event on track."
Description

Provides configurable thresholds and alerts for key risks such as open slots above target, ETA slippage, sudden drop-offs, or channel deliverability issues. Sends actionable notifications in-app and via SMS or Slack (where configured), with snooze and acknowledge controls to prevent alert fatigue. Links each alert to a recommended action (e.g., increase Boost radius, trigger a Resend wave) and records resolutions to the audit log. Integrates with the Impact Board to reflect stabilization actions and outcomes. Keeps Captains ahead of issues without constant monitoring.

Acceptance Criteria
Threshold Configuration per Campaign and Risk Type
Given I am a Captain with Manage Alerts permission When I open Captain Console > Alerts Settings Then I can create and edit threshold rules for risk types: Open Slots Above Target, ETA Slippage, Sudden Drop-off, Channel Deliverability And each rule supports scope selection: Org default, Campaign, Location, Shift type And numeric inputs are validated (percentages 1-100, minutes 1-240) with inline errors And I can choose notification channels per rule: In-app, SMS, Slack And saving a change persists within 30 seconds and is reflected on re-open And the active rules are available via API endpoint '/alerts/thresholds' including scope, values, channels, and updated_at
Open Slots Above Target Alert
Given a rule 'Open Slots Above Target > 5 for 3 minutes' exists for Campaign A And current target is 40 filled, with 8 open slots for 3 consecutive minutes When the condition is first met Then an alert with risk_type 'Open Slots Above Target' is created within 15 seconds And it includes campaign, location, shift window, current open slots, target, threshold, and timestamp And it presents a recommended action 'Increase Boost Radius' as a one-tap control And no duplicate alert is sent for the same scope within 5 minutes unless severity increases by >= 20% And when open slots drop to <= 5 for at least 2 minutes, the alert auto-resolves and a resolution event is recorded
ETA Slippage Detection and Alert
Given a rule 'ETA Slippage > 8 minutes for >= 20% of assigned' exists for Today's shifts And live ETAs show 25% of assigned with slippage >= 10 minutes When the condition is detected for 2 consecutive samples spaced 30 seconds Then an ETA Slippage alert is created within 15 seconds of the second sample And the alert lists the top 5 most-delayed assignees and their contact channels And it provides a recommended action 'Trigger Resend Wave' with pre-filled segment 'Late or No-ETA' And when slippage falls below the threshold for 3 consecutive samples, the alert resolves and the resolution is logged
Sudden Drop-off Detection and Response
Given a rule 'Sudden Drop-off >= 10% forecast decrease within 10 minutes or >= 5 cancellations within 5 minutes' exists for Event Z And 6 cancellations occur within 4 minutes causing a 12% forecast decrease When the condition is met Then a Sudden Drop-off alert is created within 15 seconds And the alert includes before/after forecast, cancellations count, and impacted shifts And it suggests actions 'Trigger Resend Wave' and 'Increase Boost Radius' And if additional cancellations raise the decrease by >= 10% more, the alert escalates instead of duplicating And when forecast recovers to within 3% of target for 5 minutes, the alert resolves and the resolution is logged
Channel Deliverability Degradation and Fallback
Given Slack and SMS are configured channels and In-app is always on And a rule monitors deliverability: 'Alert if Slack webhook failures >= 3 in 2 minutes or SMS delivery rate < 90% over last 50 sends' When Slack webhook returns HTTP 4xx/5xx 3 times within 2 minutes Then a Channel Deliverability alert is created within 15 seconds And the alert is delivered via alternate channels (SMS and In-app) with Slack marked 'degraded' And the alert includes a recommended action 'Reauthorize Slack' with a deep link to integration settings And the system retries the degraded channel with exponential backoff and logs each attempt And if all external channels are degraded, the alert is queued for retry and a persistent in-app banner is shown
Snooze, Acknowledge, and Alert Fatigue Protection
Given an active alert is displayed When the Captain taps 'Acknowledge' Then the alert status becomes Acknowledged and no repeat notifications are sent for that alert instance unless severity escalates When the Captain snoozes the alert for 15 minutes Then no notifications for that risk and scope are sent during the snooze window across all channels And available snooze durations include 5, 15, 30, 60 minutes and a custom input between 5 and 120 minutes And unacknowledged high-severity alerts escalate after 10 minutes with a distinct priority tag And the deduplication window prevents more than one notification per risk and scope within 5 minutes absent a state change
Recommended Actions, Audit Log, and Impact Board Integration
Given an alert includes recommended actions 'Increase Boost Radius' and/or 'Trigger Resend Wave' When the Captain triggers an action from the alert Then the action executes within 10 seconds and displays a confirmation with the new setting or wave ID And an audit log entry is written with fields: alert_id, risk_type, scope, threshold, observed_values, action_type, parameters, actor_id, timestamp, channels, outcome And the Impact Board shows the stabilization action tag within 60 seconds and updates outcome metrics (open slots, show-up rate) within 5 minutes And if the action fails, an error message is shown with a retry option, and a failed action entry is logged

Credit Keeper

Automatically preserves partner attribution when their volunteers fill gaps and shares only approved fields per coalition rules. Keeps co-hosts aligned and reporting clean without exposing PII.

Requirements

Persistent Partner Attribution
"As a campaign coordinator, I want volunteers automatically tied to their recruiting partner so that reporting stays accurate without manual cleanup."
Description

System captures and persists originating partner attribution for every contact, signup, donation, and shift assignment across all intake channels. Supported capture methods include partner-specific links and QR codes, embedded partner IDs in forms, source fields on CSV imports, and manual selection during admin entry. Attribution is retained through deduplication and merges via deterministic and probabilistic matching, with a configurable model (first-touch default, last-touch optional) and item-level attribution for shifts and donations. A precedence order resolves multiple sources, and historical attribution is versioned to enable backfills and rollbacks. APIs and reporting endpoints expose current and historical partner credit without requiring PII. Integrates with signup flow, importer, deduper, scheduler, donations, and the Impact Board.

Acceptance Criteria
Partner Link and QR Attribution Capture
Given a visitor arrives via a partner-specific link or QR containing partner_id=P123 and a valid, unexpired token When the visitor completes a signup, donation, or shift claim within 30 minutes of first page load in that browser Then the created contact, signup, donation, and shift records store originating_partner_id=P123 And an attribution_event is recorded with timestamp, channel (link|qr), and source_uri And if the token is invalid, expired, or revoked, Then no partner attribution is set and an audit log entry is created with reason=invalid_token
Embedded Partner ID in Forms
Given a hosted or embedded form includes hidden partner_id=P456 and signature=S generated with the org’s embed key When the submission is received and the signature validates Then the created contact, signup, donation, and shift records store originating_partner_id=P456 And client-supplied partner_id values without a valid signature are ignored and logged And the form submission succeeds even if partner_id is ignored
CSV Import Source Attribution
Given an admin maps the CSV column Partner to a valid partner_id or partner_slug When the import runs Then each created or updated contact, signup, donation, and shift record is assigned the mapped originating_partner_id And rows with unknown or disabled partners are rejected to an error file with line number and reason And the import summary reports counts for assigned, rejected, and unmatched rows
Manual Admin Attribution Selection
Given an admin is creating a contact, signup, donation, or shift in the console When coalition mode is enabled Then the Partner field is required and limited to approved partners And the chosen partner is saved as originating_partner_id and an attribution_event is recorded And subsequent edits to the partner create a new attribution_version entry with actor, timestamp, old_value, and new_value
Deduplication and Merge Preserve Attribution
Given contacts C1 and C2 are matched for merge by the deduper (deterministic match OR probabilistic match with confidence >= 0.90) When the merge is executed under the First Touch model and the precedence order is Manual > Link/QR > Embedded Form > CSV Import Then the merged contact’s originating_partner_id is taken from the surviving source per precedence and earliest timestamp And donation- and shift-level partner_attribution values remain unchanged by the contact merge And an attribution_version and merge_audit record are created enabling rollback
Configurable Attribution Model and Precedence
Given the org-level Attribution Model is set to First Touch (default) or Last Touch and a precedence list is configured When new records are created or merges occur Then partner attribution is computed according to the selected model and precedence list And changing the Attribution Model creates a new version and generates a dry-run backfill report showing impacted counts by entity And upon admin approval, a backfill job updates current partner attribution while preserving prior versions for historical queries And auto-scheduler shift fills and reminder flows do not overwrite existing item-level partner_attribution unless the Last Touch model with higher-precedence source applies
APIs and Impact Board Expose Attribution Without PII
Given an authenticated client calls attribution/reporting endpoints with scope=attribution.read When requesting current or historical partner credit for contacts, donations, or shifts Then responses include partner identifiers (id, slug), attribution_model, version, and timestamps, and exclude PII fields (name, email, phone) And clients can filter by current versus a specific version_id and by date range And Impact Board tiles display partner totals and trends based on the current attribution model without exposing PII
Gap-Fill Credit Preservation
"As a coalition lead, I want credit to follow a partner’s volunteer when they replace a no-show so that partners are recognized for keeping shifts covered."
Description

When a volunteer fills an open slot via auto-fill, waitlist, or manual reassignment, the system applies coalition-configurable rules to assign credit to the volunteer’s originating partner, preserving recognition even when coverage changes occur close to the event. Rules support full or proportional credit by hours worked, partial credit sharing with the host, and time-window constraints (e.g., replacements after reminder sends). Credit adjustments recalculate downstream metrics and posters in the Impact Board and are reflected in exports without exposing PII. Edge cases handled include split shifts, early departures, no-shows, cancellations, and cross-event replacements. All changes are recorded with reason codes for transparency.

Acceptance Criteria
Auto-Fill Replacement Preserves Originating Partner Credit
Given a coalition rule "Full Credit to Originating Partner within 48h window" is active And Host Event E has an open slot in Role R on Date D And Volunteer V is affiliated with Partner P When the system auto-fills V into the open slot Then Partner P is assigned 100% credit for that slot per rule And Host receives 0% credit for that slot And the change is recorded with reason code "Auto-Fill Replacement" And the Impact Board partner totals for Event E reflect the new credit within 60 seconds And the Activity Log shows who/what triggered the auto-fill with timestamp and rule version
Proportional Credit for Split Shifts and Early Departure
Given coalition rule "Proportional by Hours" is active And a 4-hour shift is covered by Volunteer V1 (Partner P1) for 3 hours and Volunteer V2 (Partner P2) for 1 hour due to early departure and backfill When attendance is checked out with actual hours of 3h for V1 and 1h for V2 Then P1 receives 75% credit and P2 receives 25% credit for the shift And totals for partners sum to 100% for the shift And the change is recorded with reason codes "Early Departure" and "Backfill" And the Impact Board and exports reflect the proportional credits
Partial Credit Sharing With Host per Rule
Given coalition rule "Originating Partner 70% / Host 30%" is active And Volunteer V from Partner P fills Host H's open slot When V completes the shift Then Partner P is assigned 70% credit and Host H is assigned 30% credit for that shift And totals reconcile to 100% for the shift And the allocation is included in exports as percentages/units without PII And an audit entry captures rule name, percentages applied, and reason code "Credit Share per Rule"
Waitlist Promotion After Reminder Window Applies Time-Window Rule
Given coalition rules define a "Reminder Window" as T0 and "Post-Reminder Host Share = 50%" And Volunteer V from Partner P is promoted from the waitlist after T0 When V is assigned to the open slot Then credit is split Host 50% and Partner P 50% per the time-window rule And the system stores the timestamp of promotion and the rule version used And the Impact Board and exports reflect the post-reminder allocation
Cross-Event Replacement Retains Partner Attribution and Updates Metrics/Exports Without PII
Given Volunteer V from Partner P is reassigned from Event A to fill an open slot in Event B And coalition rule "Full Credit to Originating Partner" is active for cross-event replacements When the reassignment is saved Then Partner P receives credit for Event B per rule And any credit for Event A is reduced or cleared per attendance status And exports for Events A and B show updated credits using only approved fields (no name, email, phone) And an audit trail links the change across both events with reason code "Cross-Event Replacement"
No-Shows and Cancellations Adjust Credit With Reason Codes
Given coalition rules define "No-Show = 0% credit" and "Late Cancel (<24h) = Host 50% / Originating Partner 50%" And Volunteer V from Partner P is marked No-Show for a scheduled shift When attendance is finalized Then Partner P receives 0% credit for that shift And if V is instead marked Late Cancel within 24 hours of the shift start, credits are allocated 50/50 between Host and Partner P And the selected reason code is required and stored And Impact Board and exports update accordingly
Manual Reassignment Honors Rules and Is Fully Auditable
Given a coordinator manually reassigns a shift from Volunteer V1 (Partner P1) to Volunteer V2 (Partner P2) And coalition rule "Proportional by Hours, pre-start reassignment transfers full planned hours" is active When the reassignment occurs before shift start Then P2 receives 100% of planned credit and P1 receives 0% for that shift And the system captures who performed the reassignment, timestamp, reason code "Manual Reassignment", and rule version And the Impact Board and exports reflect the new allocation within 60 seconds
Field-Level Sharing & PII Safeguards
"As a data steward, I want to share only approved, non-PII fields with co-hosts so that collaboration doesn’t risk privacy breaches."
Description

A policy engine enforces field-level sharing rules defined at coalition, campaign, and event levels, ensuring only approved, non-PII data is visible to co-hosts and partners. Administrators select from a catalog of fields and transformations (e.g., initials instead of full name, age banding, email hashing for matching, phone redaction) and set expirations and exceptions. Policies apply uniformly across UI screens, exports, scheduled reports, webhooks, and APIs. Default templates speed setup for low-IT teams, and violations are blocked with clear error messages. Data-in-transit and at-rest encryption is enforced, with access logs for all shared views.

Acceptance Criteria
Coalition Policy Enforcement Across All Channels
Given an active coalition-level sharing policy with an explicit allowedFields set and configured transformations When a co-host views volunteer data in the UI roster Then only the allowedFields are displayed and all configured transformations are applied And no disallowed fields are present Given the same policy When a CSV export is generated Then only the allowedFields appear as columns with transformed values And the CSV includes a policyVersion identifier in metadata/header Given the same policy When a scheduled report is delivered to a co-host Then the report contains only allowedFields with transformations applied And the report metadata includes policyName and policyVersion Given the same policy When a webhook is fired to a partner endpoint Then the payload contains only allowedFields with transformations applied And includes header X-Policy-Version=<policyVersion> Given the same policy When a partner API v1 request for volunteer records is made Then the response contains only allowedFields with transformations applied And the response meta includes policyVersion And field-level visibility is consistent across UI, export, scheduled report, webhook, and API for the same records and timestamp
PII Transformations: Initials, Age Bands, Email Hash, Phone Mask
Given a policy configuration with transformations And Initials rule: first letter of first name + first letter of last name, uppercased, non-ASCII preserved (e.g., “María O’Neill” -> “MO”) And Age bands: <18, 18-24, 25-34, 35-44, 45-54, 55+ And Email hash: SHA-256 of lowercased trimmed email concatenated with coalitionSalt (hex), output hex string And Phone mask: normalize to E.164, reveal last 2 digits, replace other digits with “x” and preserve leading “+” and country code length (e.g., “+14155552671” -> “+1xxxxxxxx71”) When records containing full name, dateOfBirth, email, and phone are shared under this policy Then the shared values match the configured transformation outputs exactly And no untransformed PII (full name, raw DOB, raw email, full phone) is present in any shared channel And missing or invalid inputs result in empty transformed values per policy (no fallback to raw data)
Policy Expiration and Partner Exceptions
Given a campaign-level policy with expiresAt=2025-09-30T23:59:59Z and no exceptions When any partner attempts to access shared data after the expiration timestamp Then the request is blocked with 403 POLICY_EXPIRED and no data is returned across UI, export, report, webhook, or API And scheduled reports and webhooks cease delivery after expiration Given the same policy with an exception for partnerId=partner_A until 2025-10-31T23:59:59Z When partner_A accesses data within the exception window Then access is permitted under the original policy When partner_A accesses after the exception window Then access is blocked with 403 POLICY_EXPIRED Given any expiration event When the policy expires Then coalition and campaign admins receive a system alert and the policy status is marked Expired in the admin UI
Default Templates for Low-IT Setup
Given a coalition admin onboards a new campaign When the admin applies the “Minimal Share” default template from the catalog Then a policy is created and scoped to the campaign with pre-defined allowedFields (non-PII) and standard transformations enabled And the admin can publish the policy in 3 or fewer clicks without editing any rules And the policy is immediately enforceable across UI, export, scheduled reports, webhooks, and APIs for that scope And the admin can switch to the “Attribution + Matching” template and publish a replacement policy in 3 or fewer clicks And a banner confirms the active template and policyVersion post-publish
Violation Blocking and Clear Error Messages
Given an active event-level policy that excludes raw email and phone When a partner API request includes fields=[fullName,email,phone,shiftId] Then the response is 403 POLICY_FIELD_DENIED with body containing errorCode, policyName, policyVersion, channel=API, deniedFields=[email,phone,fullName], remediationUrl And no partial data is returned Given a UI export attempt that includes a disallowed field When the user clicks Export Then the export is blocked and a modal shows the denied fields, active policy name/version, and a link to policy details Given a webhook subscription configured for disallowed fields When the policy is published Then the subscription is rejected with 422 POLICY_MISMATCH and a clear message describing required field changes
Encryption In-Transit and At-Rest Enforcement
Given any partner access over network When a TLS handshake occurs Then TLS version is >= 1.2 and weak ciphers (e.g., RC4, 3DES) are rejected And HTTP responses include HSTS header with max-age >= 15552000 And attempts over HTTP are redirected to HTTPS or rejected with 403 depending on channel Given storage of shared datasets for exports and scheduled reports When files are persisted at rest Then they are encrypted using AES-256 with keys managed by KMS and rotated at least every 90 days And key rotation events are logged and referenceable in audit Given any webhook signing configuration When payloads are delivered Then they are signed using HMAC-SHA256 with per-partner secrets and include a timestamp to prevent replay
Access Logging for All Shared Views
Given any partner or co-host views or receives shared data under a policy When the data is displayed in UI, downloaded via export, delivered in scheduled reports/webhooks, or returned by API Then an access log entry is created with timestamp (UTC ISO8601), actorId or integrationId, partnerId, channel, scope (coalition/campaign/event), recordCount, fieldSetId/transformSetId, policyVersion, and requestId And access logs are immutable (append-only), retained for at least 24 months, and searchable by partnerId, date range, channel, and scope And coalition admins can view logs in the Admin > Audit screen within 5 minutes of the event and export them as CSV And accessing the audit logs does not expose PII, only metadata and transformed field identifiers
Consent Management
"As a volunteer, I want clear control over how my information is shared across partners so that my privacy preferences are respected."
Description

Contextual consent is collected during signup and shift confirmation, presenting coalition-specific disclosures about data sharing and partner attribution. Users can opt in or out of sharing non-essential fields and change preferences later via a self-service link in receipts and reminders. The system records timestamped consent artifacts with versioned copy and jurisdiction tags, and enforces preferences in the sharing engine; when consent is missing or revoked, only aggregate metrics are shared. Supports separate SMS and email outreach consents and honors legal retention and deletion policies.

Acceptance Criteria
Signup Consent Capture with Coalition-Specific Disclosures
Given a user starts signup within a coalition context, When the signup form renders, Then the coalition-specific disclosure text matching disclosure_version_id for that coalition and the user’s jurisdiction_code is displayed before any consent choices. Given the user selects their non-essential data sharing preference (opt-in or opt-out), When they submit the signup form, Then a consent artifact is created with: user_id, coalition_id, scopes.data_sharing_nonessential (true|false), disclosure_version_id, jurisdiction_code, timestamp (UTC ISO8601), ip_address, user_agent, locale, actor=user. Given the signup succeeds, When the receipt is sent, Then the receipt includes a unique self-service consent link tied to the user/contact and coalition. Then 95th percentile consent write latency is ≤ 1 second and disclosure render time is ≤ 500 ms.
Shift Confirmation Consent Refresh and Enforcement
Given a user is confirming a shift, When the system detects the user’s last consent is older than 180 days, the disclosure_version_id has changed, or jurisdiction_code differs, Then updated disclosures are shown and the user is prompted to confirm or update non-essential sharing. Given the user skips the prompt, When no prior stored preference exists, Then scopes.data_sharing_nonessential defaults to false (opt-out). Given the user revokes non-essential sharing during confirmation, When they submit, Then sharing enforcement updates within 5 minutes and subsequent partner exports omit PII for that user. Then an append-only audit event is recorded for any change with timestamp and actor=user.
Self-Service Consent Management via Receipt Link
Given a recipient opens the self-service consent link in a receipt or reminder, When the link token is valid and unexpired (≤ 90 days), Then the user can view current consents and update: data_sharing_nonessential, sms_outreach, email_outreach. Given the user changes any setting, When they save, Then a consent artifact change event is recorded and a confirmation message (email/SMS) is sent summarizing the new preferences. Then the link token is single-use, signed, bound to the contact, and rotates on use; invalid/expired tokens return 401 without revealing PII. Then the page meets WCAG 2.1 AA form accessibility requirements.
Separate SMS and Email Outreach Consents
Given a user provides a phone number and/or email, When setting outreach preferences, Then sms_outreach and email_outreach are captured and enforced independently. Given jurisdiction_code requires double opt-in for SMS, When the user requests sms_outreach=true, Then an opt-in confirmation message is sent and sms_outreach remains false until the user confirms; the confirmation is timestamped and stored. Given sms_outreach=false or email_outreach=false, When campaigns are queued, Then messages to that channel are suppressed within 5 minutes and the contact appears on the channel-specific suppression list. Then suppressed message attempts are counted in reporting with suppression_reason=consent.
Consent Artifact Logging with Versioning and Jurisdiction Tags
Given any consent capture or change occurs, When the event is persisted, Then an immutable consent_event is appended with: event_id, user_id, coalition_id, partner_ids (if applicable), scopes, channel_consents (sms,email), disclosure_version_id, jurisdiction_code, timestamp, actor (user|system|admin), source (signup|shift_confirmation|self_service), ip_address, user_agent. Given an admin requests an export for a coalition and a date range ≤ 31 days and ≤ 100k events, When the export runs, Then CSV and JSON files are generated within 30 seconds containing exactly the recorded fields. Given concurrent updates, When events are queried, Then ordering by timestamp and event_id is consistent and prior events are never mutated.
Sharing Engine Respects Consent and Falls Back to Aggregates
Given the sharing engine prepares a partner export, When a user’s data_sharing_nonessential is false or missing, Then only aggregate metrics (counts, sums) are included; no PII or non-essential fields for that user are present. Given data_sharing_nonessential=true, When exporting to partners, Then only fields approved by coalition rules for that partner are shared and partner attribution (source_partner_id) is preserved. Given consent is revoked, When the next export runs, Then the user’s record is removed from PII-bearing detail feeds within 5 minutes and appears only in aggregates. Then unit and contract tests reject any export that includes PII for users without consent.
Retention and Deletion Policy Enforcement for Consent Data
Given a deletion request is received for a user and allowed by jurisdiction and retention policy, When the deletion job runs, Then direct identifiers (name, email, phone) are purged within 30 days from primary stores and from backups within an additional 30 days. Given retention rules require keeping consent audit for a configured period per jurisdiction_code, When PII is deleted, Then consent_event records are retained for that period with identifiers irreversibly pseudonymized and no contact data present. Given an export or report runs after deletion, When the user was deleted, Then no PII appears and aggregates remain unaffected. Then a deletion_event is recorded with jurisdiction_code, legal_basis, actor, and timestamps.
Partner-Scoped Dashboards & Exports
"As a partner organizer, I want a dashboard showing my credited shifts and hours with only allowed details so that I can report outcomes without seeing PII."
Description

A partner-scoped dashboard provides read-only, mobile-friendly views of credited metrics—signups, hours covered, donations influenced—filtered by the partner’s scope and governed by the sharing policy. List views and exports include only approved fields; sensitive PII is never rendered. Partners can schedule email exports or pull via API tokens scoped to their organization. Role-based access ensures only authorized partner users can view their data, with simple invite flows suitable for small groups. Impact Board tiles include per-partner slices to showcase contribution without revealing identities.

Acceptance Criteria
Mobile Dashboard, Read-Only, Partner Scope
Given a signed-in partner user with Viewer role scoped to Partner A When they open the partner dashboard on a smartphone viewport (≤414px width) and select a date range Then only Partner A’s credited totals for signups, hours covered, and donations influenced are displayed And no create/edit/delete controls are rendered And the page fully loads within 2 seconds on a standard 4G connection with 100 ms RTT And charts and tiles fit within the viewport without horizontal scrolling And changing the date range updates all metrics consistently within 1 second
List Views and Exports Enforce Approved Fields Only
Given a partner user views a list or requests a CSV export via the dashboard When the sharing policy approves fields (e.g., event_date, event_name, partner_credit, shift_hours, donation_amount_range) Then only the approved fields are visible/included in the export in the approved order And PII fields (full_name, email, phone, address, freeform_notes, donor_id) are never rendered or exported And attempts to include unapproved fields via column picker or query parameters are rejected with a clear validation message and are not returned And CSV exports are UTF-8, include headers, and contain 0 PII values across a sample of 100 records
Attribution Preserved in Partner Metrics for Gap-Fill Volunteers
Given volunteers affiliated with Partner A fill open shifts on events hosted by Partner B When partner-scoped metrics are computed Then hours covered, signups, and donations influenced from those volunteers are credited to Partner A per coalition rules And Partner B’s metrics are not double-counted for the same activity And Partner A’s dashboards and exports reflect these credited counts within 15 minutes of activity capture And no volunteer identity fields are exposed in either partner’s views
Scheduled Email Exports for Partners
Given a partner user with Exporter role for Partner A When they schedule a CSV export by selecting frequency (daily/weekly/monthly), time, timezone, and recipients (Partner A users) Then the schedule is saved and shown in Schedules with next run time And the first export is delivered at the next scheduled time with only approved fields and Partner A–scoped data And emails contain a time-bounded, signed download link that requires authentication And if the file size exceeds 10 MB, the email omits attachments and provides only the download link And pausing or deleting the schedule stops further deliveries within 5 minutes And an audit entry records schedule creation, edits, runs, failures, and delivery outcomes
Partner-Scoped API Tokens for Data Pulls
Given a Partner A admin creates an API token with read scope When the token is used to call partners/{partner_id}/metrics or partners/{partner_id}/records Then responses include only Partner A–scoped data and only approved fields And calls specifying a different partner_id return 403 Forbidden And requests exceeding 60 requests per minute per token are rate-limited with HTTP 429 And revoked tokens immediately return 401 Unauthorized on subsequent calls And each API call is audit-logged with token id, route, timestamp, and response code
Role-Based Access and Simple Invite Flow
Given a Partner A admin sends an invite by email assigning Viewer or Exporter role When the invitee accepts via magic link within 72 hours and completes sign-in Then the user gains access only to Partner A dashboards/exports per their role And attempts to access other partners’ data return 403 Forbidden And removing or downgrading the user revokes elevated access within 5 minutes And expired or already-used invites are rejected with clear guidance to request a new invite
Impact Board Partner Slices Without Identities
Given the Impact Board is displayed with partner slicing enabled When Partner A is part of the coalition Then a tile shows Partner A’s aggregate contribution (signups, hours, donations influenced) without any record-level details And no names, emails, phone numbers, or other PII are visible anywhere on the tile And clicking the tile reveals only aggregate charts filtered to Partner A without PII And partners with zero contribution display 0 without revealing identities
Attribution Audit & Dispute Resolution
"As an operations manager, I want an auditable way to review and resolve attribution disputes so that coalition reports remain trusted."
Description

A complete audit trail captures every change to partner attribution and shared data visibility, including actor, timestamp, source, prior value, new value, and reason code. Partners can submit disputes or questions from within their dashboard against specific records; the workflow assigns the case to coalition admins, supports attachments and comments, and tracks SLA. Approved re-attribution triggers consistent recalculation of reports and backfills dependent exports. Period-close snapshots lock monthly totals while allowing late corrections to roll into the next cycle, preserving trust in published numbers.

Acceptance Criteria
Immutable Audit Trail for Attribution Changes
Given a record with current partner attribution, When an authorized actor manually changes the partner attribution and selects a reason code, Then an audit entry is written capturing actor ID and role, UTC timestamp, source=manual UI, record ID, field changed, prior value, new value, reason code, and a unique audit ID. Given an automated attribution change triggered by Credit Keeper, When the change is committed, Then an audit entry with the same fields is written with source=automation and includes the automation rule ID. Given audit entries exist for the record, When any user attempts to edit or delete an existing audit entry, Then the system rejects the attempt with 403 Forbidden, logs a security event, and the audit entry remains unchanged. Given a coalition admin views the audit trail for the record, When results are returned, Then entries are ordered by timestamp descending and are filterable by field and date range. Given a partner user views the audit trail for an accessible record, When the audit list is displayed, Then only non-PII fields and values are shown per coalition visibility rules.
Partner-Initiated Dispute Submission on Specific Record
Given a partner dashboard user with access to record R, When they click Dispute Attribution on R, select a reason code from the allowed list, add an optional comment, and attach up to 3 files (PDF/JPG/PNG) each ≤10 MB, Then a dispute case is created with ID format GC-DSP-YYYYMM-####, linked to R and the submitting partner, storing attachments, and an SLA timer is started. Given an open dispute already exists for record R, When the user attempts to submit another dispute on R, Then the system prevents duplicate submission and provides a link to the existing case. Given a case is created, When notifications are sent, Then the submitting partner receives confirmation and the appropriate coalition admin queue receives a new-case alert.
Case Assignment, Collaboration, and SLA Tracking
Given routing rules by coalition and record type, When a new dispute is created, Then it is placed in the correct admin queue with status Open and an owner is assigned within 5 minutes. Given a case is open, When admins or partners add comments, Then internal (admin-only) and external (partner-visible) comments are clearly labeled, time-stamped, and external comments notify the opposite party. Given SLA targets of first response=2 business days and resolution=7 business days are configured, When the case is created, Then countdown timers are displayed and breach warnings are escalated to admins 4 hours before each breach. Given a case breaches SLA, When the breach occurs, Then the case is marked Breached, an escalation notification is sent to coalition leads, and SLA metrics are updated. Given a case is resolved as Approved or Denied with a resolution reason, When resolution is saved, Then the partner is notified, SLA timers stop, and the case becomes read-only except for post-resolution comments.
Approved Re-Attribution Recalculates Reports and Backfills Exports
Given a dispute case on record R is approved to re-attribute from partner A to partner B, When the resolution is saved, Then the system updates R's partner attribution and writes a corresponding audit entry with prior and new values and reason code. Given the re-attribution is applied, When aggregate reports (partner rollups, Impact Board, show-up rates, donation totals) refresh, Then all affected totals reflect the change consistently within 15 minutes across dashboards. Given dependent exports (daily CSV and API feeds) exist, When the next scheduled export run occurs, Then backfilled exports include corrected records and a change-log row referencing the audit ID. Given the same case is processed again, When the system evaluates the change, Then no duplicate changes occur (idempotent), and aggregates remain consistent.
Period-Close Snapshot Locks Totals and Rolls Corrections Forward
Given the monthly close is scheduled for 23:59:59 UTC on the last day of the month or triggered by a coalition admin, When the close executes, Then a read-only snapshot is created storing partner totals and key metrics with a snapshot ID and cryptographic hash. Given a snapshot exists for Month M, When a late correction (including re-attribution) dated in Month M is approved after the close, Then Month M published totals remain unchanged and an adjustment entry is queued to roll forward into Month M+1. Given the next period opens, When roll-forward adjustments are applied, Then Month M+1 reports show an Adjustments line item and totals reflect the changes with links to the original Month M records. Given an admin attempts to modify a snapshot, When the change is attempted, Then the system blocks edits (read-only) and logs an audit/security event.
Audit and Enforcement of Shared Field Visibility (No PII Leakage)
Given a coalition admin changes the shared field visibility rule from [Name, Phone, Email] to [First Name only], When the change is saved, Then an audit entry records actor, timestamp, prior visibility set, new visibility set, source=admin UI, and reason code. Given the visibility rule change, When a partner user views records, downloads exports, or calls the API, Then only the approved fields are present and PII fields are excluded within 10 minutes of the change. Given an unauthorized request attempts to fetch restricted fields, When the request is made, Then the system returns 403 or omits the fields and logs an access denial event tied to the actor and timestamp. Given a rollback to the prior visibility is performed, When the change is saved, Then the audit trail captures the rollback and downstream views/exports reflect the prior visibility within 10 minutes.

Consent Ledger

A time-stamped, partner-visible log of each contact’s sharing consent, channel, and scope. MergeGuard checks this ledger before any dedupe or export, auto-blocking records outside agreed terms. Stewards stay compliant without policy spreadsheets; partners trust that outreach honors what people actually allowed.

Requirements

Append-only Consent Ledger
"As a data steward, I want an append-only consent history with timestamps, channels, and scopes so that I can prove and reconstruct what each person allowed at any point in time."
Description

Implement an immutable, append-only event ledger for each contact that records consent events with precise timestamps, channel of capture (web, SMS, phone, paper), scope (purpose, sharing partners, retention), actor, source system, terms/policy version, locale/jurisdiction, and evidence pointers. Compute the contact’s effective consent state on read via event reduction, exposing a normalized schema to forms, imports, and APIs. Embed a ledger view on the contact profile and ensure idempotent writes and deduplicated event ingestion. This centralized history replaces ad-hoc spreadsheets, strengthens auditability, and becomes the single source of truth for downstream enforcement and reporting.

Acceptance Criteria
Immutability and Append-only Enforcement
Given an existing consent ledger event, When a client attempts to update it via API, Then the request is rejected with 405 Method Not Allowed and no event is modified. Given an existing consent ledger event, When a client attempts to delete it via API or UI, Then the operation is blocked and no event is removed. Given a valid new consent event payload, When appended, Then exactly one new event is created and the total event count increases by one for that contact. Given the ledger contains N events for a contact, When the integrity verification job runs, Then 100% of events pass append-only verification and any failure flags the contact ledger as suspect and emits an alert within 1 minute.
Event Schema Validation and Required Fields
Given a create event request missing any required field (occurred_at, channel, scope.purpose, scope.partners, actor.id, actor.type, source_system, policy_version, locale, jurisdiction, evidence.pointer), When submitted, Then the API responds 400 with field-specific error codes and nothing is persisted. Given a create event request, When occurred_at is not ISO-8601 UTC with millisecond precision, Then the API responds 400 with error code occurred_at_format. Given a create event request, When channel is not one of {web, sms, phone, paper}, Then the API responds 400 with error code channel_invalid. Given a create event request, When locale is not a valid BCP 47 tag or jurisdiction is not ISO 3166 compliant, Then the API responds 400 with error codes locale_invalid or jurisdiction_invalid. Given a create event request with scope.retention.expires_at earlier than occurred_at, When submitted, Then the API responds 400 with error code retention_invalid.
Idempotent Writes and Deduplicated Ingestion
Given a create event request with idempotency_key K, When the same request with K is retried within 24 hours, Then the API returns 200 with the original event id and no additional event is created. Given two create event requests with identical evidence.fingerprint and identical payloads, When processed, Then only one event is persisted and the second returns 200 referencing the first event id. Given a second request reusing idempotency_key K but with a different payload, When submitted, Then the API responds 409 Idempotency Conflict and no new event is created. Given a batch ingestion job containing duplicate rows (same evidence.fingerprint), When processed, Then the job result reports duplicates as skipped and the ledger event count increases only by the number of unique rows.
Effective Consent State Reduction on Read
Given a contact ledger containing grant and withdraw events across multiple partners and purposes, When GET /contacts/{id}/consent is called, Then the response reflects the latest event by occurred_at per purpose and partner and any withdraw overrides prior grants. Given a contact ledger where retention has expired for a purpose, When GET /contacts/{id}/consent is called, Then the effective state for that purpose is expired and sharing is disabled. Given a contact with no grant for a purpose in a jurisdiction requiring explicit consent, When GET /contacts/{id}/consent is called, Then the effective state defaults to no_share for that purpose. Given a contact with up to 1,000 consent events, When GET /contacts/{id}/consent is called, Then the endpoint responds with P95 latency ≤ 200 ms over 1,000 requests.
Contact Profile Ledger View and Evidence Access
Given a contact with consent events, When viewing the contact profile, Then the ledger shows the most recent 50 events sorted by occurred_at descending with pagination available for older events. Given the ledger view, When an event row is rendered, Then it displays channel, purpose, partners, retention summary, actor, source_system, policy_version, locale, jurisdiction, and an evidence link. Given a user clicks an evidence link, When the artifact exists, Then it is retrieved within 2 seconds; When it does not exist, Then the UI shows an error state with code evidence_unavailable and the event remains visible. Given the contact profile loads, When the effective consent badge is displayed, Then its values exactly match GET /contacts/{id}/consent for the same contact at the time of render.
MergeGuard Enforcement on Dedupe and Export
Given a user initiates a contact merge, When the combined records would violate consent scope or partner restrictions, Then MergeGuard blocks the merge and displays reason code conflicting_consent_scopes with links to each ledger. Given an export job to Partner P for Purpose X, When a contact’s effective consent does not include P for X, Then the record is excluded from the payload, a per-record failure with reason consent_blocked is produced, and no excluded record appears in the exported file. Given a user attempts to override a MergeGuard block, When issuing the request via UI or API, Then the operation is rejected with 403 Forbidden and an audit entry is recorded with user id, timestamp, and reason.
Normalized Schema Exposure to Forms, Imports, and APIs
Given a public web form capturing consent, When a user submits consent, Then a ledger event is appended with channel=web and the policy_version provided by the form, and GET /contacts/{id}/consent reflects the new state within 2 seconds. Given an SMS keyword flow and an admin phone entry, When consents are captured, Then events are appended with channel=sms and channel=phone respectively and evidence.pointer references the message id or call recording id. Given a CSV import with consent columns and batch_id B, When processed, Then valid rows create events, invalid rows are rejected with a per-row error report, and re-running the same file with batch_id B results in zero additional events created. Given GET /contacts/{id}/consent is called, When the response is validated, Then it conforms to the published normalized schema including fields effective.consent_per_purpose, effective.partners_allowed, effective.expires_at, effective.jurisdiction, and policy_version under API version v1.
MergeGuard Pre-Flight Enforcement
"As an organizer, I want the system to automatically block merges and exports that violate consent so that we avoid accidental noncompliance."
Description

Create a policy engine that intercepts dedupe/merge, export/sync, and bulk messaging operations to evaluate each record’s effective consent against partner agreements and org defaults. Automatically block or filter non-compliant records, return actionable reason codes to the UI/API, and log attempted violations. Support per-partner rules, default deny when ambiguous, and real-time checks for single-record actions as well as scalable batch mode for lists. Integrate with existing merge, export, and campaign modules without requiring workflow rewrites.

Acceptance Criteria
Dedupe/Merge Pre-Flight Consent Enforcement
Given partner P and two contact records A and B with differing consent scopes When a user initiates a dedupe/merge of A and B Then the operation is blocked if the resulting merged record would broaden consent beyond the intersection of A and B under partner P And the UI/API receives reason_code "CONSENT_SCOPE_CONFLICT" with details including record_ids, disallowed_channels, and disallowed_scopes And no merge is persisted
Export/Sync Filtering by Partner Agreement
Given an export or sync job to integration I under partner P specifying channel X and purpose Y with a list of N contacts When MergeGuard preflight evaluates the job Then only contacts whose effective consent includes channel X and purpose Y under partner P are included in the payload And excluded contacts are omitted and reported with per-contact reason_code via the job results API And the job summary reports included_count and excluded_count grouped by reason_code And no data for excluded contacts is transmitted to integration I
Bulk Messaging Exclusion Enforcement
Given a bulk messaging campaign via channel C with declared purpose S under partner P When launch is requested Then only contacts with explicit consent for channel C and purpose S under partner P are queued And contacts with missing, expired, revoked, or ambiguous consent are excluded by default deny And the preflight report displays included_count and excluded_count by reason_code And no messages are enqueued for excluded contacts
Real-Time Single-Record Consent Check SLA
Given a user attempts to send a single message from a contact profile under partner P When the send action is invoked Then the consent check completes in ≤300 ms at p95 and ≤800 ms at p99 And if consent is insufficient, the action is blocked, no message is queued, and a reason_code is returned to the UI/API
Actionable Reason Codes and Audit Logging
Given any operation (merge, export, sync, bulk message, single send) is evaluated by MergeGuard When a record is blocked or filtered Then the UI/API receives a stable, documented reason_code and a user-readable reason_message suitable for remediation And an audit log entry is written with timestamp (UTC), actor_id (or system), partner_id, operation, record_id(s), decision, reason_code, and correlation_id And attempted violations are queryable by date range, partner_id, operation, and reason_code
Per-Partner Rules Precedence and Default-Deny
Given partner P has a consent ruleset and the org has default rules When evaluating effective consent for an operation scoped to partner P Then partner P rules take precedence over org defaults And conflicts are resolved to the most restrictive interpretation And ambiguous or missing consent results in a default deny decision And evaluation results include the rule_set_id and version applied
Scalable Batch Mode and Drop-In Integration
Given batch operations (export, bulk messaging) are initiated via existing modules without workflow changes When MergeGuard preflight is enabled Then module APIs, user flows, and job definitions remain unchanged except for additional preflight outcomes and reason codes And the engine processes at an effective throughput of ≥5,000 records/second with p95 completion under 5 minutes for 100k records in the standard environment And results are streamable in pages via existing job results interfaces without timeouts
Partner-Scoped Visibility & Access Controls
"As a partner administrator, I want to see only the consents relevant to our agreement so that our team stays within allowed boundaries."
Description

Introduce role- and partner-scoped access so partners and volunteers can view only consent entries and fields permitted by their agreements. Provide a consent tab within the contact profile that filters scopes by partner, shows effective status, and redacts out-of-scope details. Record a full audit trail of views, exports, and policy decisions. Offer simple role presets for small teams and an API for partners to verify consent status without exposing unrelated data.

Acceptance Criteria
Partner user views consent tab for a contact
Given a signed-in partner user with agreement A opens contact C's profile When the user selects the Consent tab Then only consent entries whose scope intersects agreement A are visible And out-of-scope fields are replaced with a Redacted placeholder And the Effective Status per visible scope displays current value, timestamp, and source reference And no out-of-scope raw values are present in the page DOM or underlying API response payloads And switching the partner filter (for multi-partner stewards) updates the visible scopes accordingly
Role presets constrain field visibility and actions
Given the system role presets are enabled Then the following presets exist: Org Admin, Data Steward, Partner Lead, Volunteer Viewer And Volunteer Viewer: may view in-scope Effective Status only; cannot view raw consent text, export, or audit logs And Partner Lead: may view in-scope Effective Status and raw consent text; may request exports limited to in-scope fields; may view audit entries for their partner only And Data Steward: may view all partners' consent entries; may not export out-of-scope fields for any partner; may view full audit logs And Org Admin: may configure roles and partner scopes; may not export out-of-scope fields for any partner And any attempt to perform a disallowed action returns HTTP 403 or UI error state with a clear reason
MergeGuard blocks out-of-scope dedupe and export
Given a user initiates deduplication or an export involving contact data When the operation would expose fields outside partner agreement A for partner P Then MergeGuard blocks the operation with error code MG-403 and a human-readable reason listing the violating scope(s)/channel(s) And the attempted operation is recorded in the audit trail with decision=Block And no role can override to include out-of-scope fields; overrides (Org Admin only) may proceed only if the resulting output excludes all out-of-scope fields
Audit trail for views, exports, and policy decisions
Given any consent tab view, API verification call, export attempt, dedupe action, or role/scope change occurs Then an audit record is written within 1 second containing: timestamp (UTC ISO 8601), actor ID, actor role, partner, contact ID(s), action, scopes referenced, channel(s), decision (Allow|Block), client IP/agent And audit records are append-only; no role can delete or edit; corrections require a new record with reason and link to prior record And authorized users can query by date range, actor, partner, action, and contact, with responses under 2 seconds for up to 10,000 matching records And export of audit logs is permitted to Org Admin and Data Steward; Partner Lead can only view/filter records for their own partner
Partner consent verification API returns only scoped data
Given a partner bearer token scoped to partner P When calling GET /api/consent-status?contact_id={id} Then the response contains only Effective Status values for scopes allowed to partner P and no unrelated fields And unauthenticated requests return 401; tokens without access to the contact return 403; unknown contact within partner context returns 404 And responses include request_id and are logged in the audit trail And rate limits enforce at least 600 requests per minute per partner; exceeding returns 429 with Retry-After And p95 latency is ≤ 1000 ms for a sustained load of 50 requests per second
Redaction behavior in UI and exports
Given a partner user views a contact or downloads an export When fields fall outside the user's partner scope Then the UI shows a Redacted badge without tooltips or content leaks And network responses and DOM contain no out-of-scope values And CSV/JSON exports omit out-of-scope columns/keys and include metadata listing excluded_fields And automated tests assert the absence of out-of-scope values in payloads and files
Effective consent resolution across channel and scope
Given multiple consent entries exist for a contact across channels and scopes When computing Effective Status per channel/scope Then the most recent timestamp wins, with precedence rules: Withdrawn > Denied > Expired > Allowed > Not Collected for ties And expirations render status=Expired once past expiry time And each displayed status shows the as_of timestamp and source event ID And UI/API updates reflect new consent entries within 5 seconds of write And conflicting same-timestamp entries resolve deterministically per precedence rule
Revocation, Expiry, and Scope Change Propagation
"As a compliance lead, I want revocations and expiries to immediately suppress outreach and notify owners so that we honor people’s choices in real time."
Description

Enable immediate suppression and propagation when a contact revokes consent, reaches an expiry date, or narrows scope. On event ingestion, recalculate effective consent and cancel queued sends, remove from upcoming shifts or exports when out-of-scope, and notify record owners. Support scheduled re-evaluations for expiring consents, grace periods where defined, and lightweight web/SMS flows for contacts to update preferences. Ensure downstream integrations receive updates via webhooks and that suppressed records remain blocked until new consent is captured.

Acceptance Criteria
Immediate Suppression on Real-Time Revocation
Given a contact revokes consent via SMS STOP, web unsubscribe, or API event type=revocation with a valid contact identifier When the event is ingested Then the system recalculates effective consent and marks the contact as suppressed for all affected scopes within 60 seconds And all queued outbound messages in affected scopes are canceled and removed from send queues with cancel_reason=consent_revoked And the contact is removed from future shift assignments and reminders tied to affected scopes occurring after the event timestamp And the contact is excluded from pending exports that include affected scopes, with exclusion reason recorded And a Consent Ledger entry is appended with timestamp, channel, scope, actor, and reason=revocation And in-app banners and API responses reflect the suppressed state on the contact record
Scheduled Expiry Re-Evaluation with Grace Period
Given a consent with expiry_date=t1 and grace_period_days=G (G may be 0) When the scheduled evaluator executes (every 15 minutes) Then if current_time < t1 the consent remains active and no notifications are duplicated And if t1 <= current_time < t1+G the consent transitions to in_grace for scopes where grace_allowed=true; otherwise those scopes transition to expired And if current_time >= t1+G all affected scopes are expired and the contact is suppressed for those scopes And queued sends/reminders/exports in expired scopes are canceled within 60 seconds of evaluation with cancel_reason=consent_expired And a pending-expiry notification is sent to the contact and record owner pending_expiry_notice_days (default 7) before t1, and a single transition notification is logged on entering in_grace or expired And all transitions append a Consent Ledger entry and are idempotent across re-evaluations
Scope Narrowing Propagation Across Channels and Activities
Given a contact narrows scope from All to Email-only at time t When effective consent is recalculated Then outbound SMS, voice, and social DMs to that contact are blocked, while email remains allowed And any queued items on disallowed channels are canceled within 60 seconds with cancel_reason=scope_narrowed And the contact remains enrolled only in activities that rely on allowed channels; activities requiring disallowed channels are removed And exports include the contact only when export.scope ⊆ allowed_scopes; otherwise the contact is excluded with exclusion reason recorded And MergeGuard blocks dedupe/merge operations that would broaden effective scope with error code=CONSENT_SCOPE_CONFLICT and no data change occurs
Downstream Webhook Update Delivery and Reliability
Given any consent change event (revocation, expiry, scope change) for a contact When the event is committed to the Consent Ledger Then a webhook is delivered to all subscribed integrations within 15 seconds containing: event_id, event_type, contact_id, effective_scopes, suppressed, occurred_at, version And the webhook is signed with HMAC-SHA256 using the partner secret and includes an Idempotency-Key header And delivery uses exponential backoff with at least 6 retries over 30 minutes and stops on any 2xx response And duplicate deliveries are de-duplicated by Idempotency-Key on the receiver; the system will not resend after a 2xx with the same key And after final failure the event moves to a dead-letter queue, the partner is alerted, and an operator can replay from the dead-letter queue
Lightweight Web/SMS Preference Update Flow
Given a contact initiates a preference update via SMS keyword "PREFS" or via a magic-link in email/SMS When the preference page loads on a mobile device Then the page renders in under 2 seconds on a 3G connection and shows current channel scopes with toggle controls And the session is verified by a signed, single-use, 15-minute magic link bound to the contact identifier And changes are applied only after explicit confirmation by the contact and a confirmation message is sent via the initiating channel And each change appends a Consent Ledger entry with actor=contact, channel, scope_before, scope_after, and timestamp And the flow meets WCAG 2.1 AA for forms and contrast and localizes to the partner's configured languages
Suppression Persistence Until New Consent Captured
Given a contact is suppressed for a scope When a user or automation attempts to send, export, assign, or merge in a way that would violate the suppression Then the action is blocked with HTTP 403 (API) or a UI error banner including code=CONSENT_SUPPRESSED and no side effects occur And the contact shows a "Suppressed" badge with a tooltip indicating reason and timestamp And export job summaries include counts of excluded contacts by reason and list the contact in the exclusion report And suppression persists across sessions and processes until a new valid consent is captured and written to the Consent Ledger And new consent lifts suppression only for the granted scopes and does not re-queue previously canceled items
Owner Notification and Tasking on Consent Changes
Given a consent change affecting a contact owned by one or more stewards When the change is committed to the Consent Ledger Then each owner receives an in-app notification within 60 seconds and an email within 5 minutes summarizing the change, scope, and next steps And a follow-up task is auto-created on the contact with due_date = 2 business days from change and linked to the ledger entry And notifications are de-duplicated so multiple changes within 10 minutes produce a single roll-up And owners who opted out of email receive only in-app notifications; all notification sends are logged And if no owner exists, the notification routes to the default team inbox
Consent Evidence & Cryptographic Receipts
"As a data steward, I want to attach proof and generate verifiable receipts so that any partner can independently verify consent."
Description

Allow attaching evidence artifacts (signed forms, call recordings, form payloads) to ledger events with content hashing for integrity and secure storage controls. Generate a human-readable, partner-verifiable consent receipt containing timestamp, scope, terms version, evidence hash, and a verification URL. Surface receipt links in the contact profile and partner portal, and expose a verify endpoint for third parties to confirm consent status without accessing unrelated personal data.

Acceptance Criteria
Evidence Attachment & Content Hashing
Given a steward attaches evidence (PDF, audio, JSON, image) to a consent ledger event When the upload completes Then the system computes a SHA-256 hash over the exact file bytes (JSON canonicalized as UTF-8 without whitespace changes) and stores the hex hash on the event And evidence is accepted only if MIME type is one of [application/pdf, application/json, image/png, image/jpeg, audio/mpeg, audio/wav] and size <= 50 MB And the API returns evidenceId and evidenceHash within 2 seconds for files <= 10 MB And the stored hash remains immutable once the event is committed
Secure Storage & Access Controls for Evidence
Given evidence is stored for a consent event When any user attempts to access or download the evidence Then the file is encrypted at rest (AES-256) and served only via time-limited signed URLs (<= 15 minutes) And only Steward or Admin roles may download evidence; Partner role can view the receipt but not the raw evidence And all access attempts (allow/deny) are audit-logged with userId, timestamp, action, evidenceId And evidence previews redact or omit unrelated personal data
Human-Readable Consent Receipt Generation
Given a consent event is saved with at least one evidence attachment When the event is committed Then a human-readable receipt is generated containing: receiptId, eventTimestamp (UTC ISO-8601), contactPseudonymousId, channel, consentScope, termsVersion, evidenceHash(es), verificationUrl And the receipt is available within 1 second of commit And the receipt content renders consistently on mobile and desktop And the receipt contains no direct identifiers (name, email, phone, address)
Receipt Links in Contact Profile and Partner Portal
Given a user opens a contact profile or a partner opens the portal When viewing the Consent Ledger list and detail views Then each applicable event shows a View Receipt link and a Copy Verify URL action And opening the receipt does not expose donation history, shift assignments, or other unrelated data And the partner portal shows receipts only for contacts shared with that partner And links resolve within 500 ms for 95th percentile
Public Verify Endpoint with Minimal Disclosure
Given a third party calls GET /api/consent/verify?rid={receiptId}&sig={token} When the token is valid and unexpired Then the response includes only: receiptId, status [active|revoked|expired], eventTimestamp, consentScope, termsVersion, evidenceHashMatch [true|false], verificationOutcome [pass|fail] And no PII fields (name, email, phone, address, donation/volunteer history) are present And invalid or expired tokens return 401 with no body fields other than code and message And requests are rate-limited to 60 per minute per IP and complete within 300 ms at p95
Integrity and Tamper Detection
Given stored evidence is altered after attachment or does not match its recorded hash When the verify endpoint recomputes the SHA-256 hash server-side Then evidenceHashMatch=false and verificationOutcome=fail are returned And evidence downloads for that item are blocked and a high-severity audit event is written And the original receipt remains immutable but visibly flagged for integrity failure on subsequent views
Revocation and Expiry Reflected in Receipts
Given a consent is revoked by the contact or expires per the applicable terms version When the receipt is viewed or the verify endpoint is called Then the status reflects revoked or expired, includes revocationTimestamp or expiryTimestamp, and verificationOutcome=pass if evidence hash matches And the corresponding ledger event in the UI updates within 2 seconds to show the new status
Multi-Channel Capture & Offline Sync
"As a field volunteer, I want to capture consent on phones, text, or paper—even offline—so that signups in low-connectivity environments are compliant."
Description

Provide consent capture across web forms, SMS keywords (e.g., YES/STOP), phone call notes, and photographed paper cards, with automatic timestamping, channel tagging, and geostamping where available. Build an offline-first mobile flow that queues events on volunteer devices with local encryption and conflict resolution, syncing to the ledger when connectivity resumes. Include double opt-in patterns for SMS/email and lightweight validation to prevent duplicate consents.

Acceptance Criteria
Web Form Consent Capture with Metadata
Given a public GiveCrew web form with a consent control and optional location permissions enabled by the browser When a contact submits the form Then the Consent Ledger stores an event with: contact_id, scope, channel="web", source_form_id, ISO-8601 UTC server_received_at within 1 second of receipt, device_captured_at, and geostamp if provided (lat/long with <=100m accuracy) else geostamp=null. And Then the event is immutable, assigned a unique event_id, and appears in the ledger within 2 seconds of submission under nominal load (<=50 rps). And Then PII fields are validated (email RFC 5322; phone E.164) and invalid submissions are rejected with a 400 error and no ledger event created. And Then if an identical consent exists for the same contact_id, scope, and channel within the last 24 hours, the system suppresses a new event and appends a dedupe_note to the existing event.
SMS Keyword Double Opt-In and STOP Handling
Given a contact texts a published campaign keyword to an org long code or short code When the message is received Then the system sends a double opt-in prompt within 5 seconds and records a pending opt-in event with channel="sms" and status="pending". And When the contact replies YES within 30 days Then the ledger updates with a confirmed opt-in event referencing the pending event and marks status="confirmed"; otherwise the pending event expires with status="expired" and is not usable for outreach. And When the contact sends STOP at any time Then the ledger records an opt-out event within 2 seconds, blocks all outbound SMS to that number immediately, and sends a STOP confirmation reply; subsequent messages are blocked until a new confirmed double opt-in event is recorded. And Then YES/STOP matching is case-insensitive and ignores leading/trailing whitespace and punctuation.
Phone Call Notes Consent Capture
Given an organizer is on a call and has the mobile app offline or online When the organizer records verbal consent with selected scope and contact identifier Then the app enforces required fields (scope, consent_state, contact_id) and saves the event locally with device_captured_at and channel="phone". And When connectivity is available Then the event is synced to the ledger within 60 seconds, server assigns server_received_at and event_id, and the local record is marked synced. And Then if the organizer attempts to submit without required fields, the app blocks save and displays inline errors; no partial ledger event is created.
Photographed Paper Card Consent
Given a volunteer photographs a paper signup card in the mobile app When the image is captured and the volunteer selects the consent scope Then the app attaches the photo, attempts OCR, and queues a consent event with channel="paper" and device_captured_at. And Then OCR-extracted email/phone are validated; if OCR confidence < 0.70 or validation fails, the event is flagged review_required=true but still queued; no outbound messages are sent until review is cleared. And When synced Then the ledger stores the event with source_device_id and image_url, and emits a task for review if review_required=true.
Offline Queue, Encryption, and Sync Reliability
Given the device has no connectivity When a consent event is created Then it is written to a local encrypted store using AES-256-GCM with keys managed by the OS keystore/Keychain and is unreadable if the user is logged out. And When connectivity resumes Then all queued events attempt sync within 60 seconds, using exponential backoff up to 24 hours on failure, and show a visible unsynced count badge to the user. And Then each synced event’s payload integrity is verified via SHA-256 hash; if verification fails, the event is retried; after 5 consecutive failures the app surfaces an error and leaves the event unsynced.
Conflict Resolution and Duplicate Prevention
Given multiple consent events exist for the same contact and scope from any channels When events conflict (e.g., opt-in vs opt-out) Then the ledger maintains an immutable history and computes effective consent as: opt-out supersedes prior opt-ins, and only a later confirmed double opt-in (timestamp > opt-out) restores opt-in. And Then duplicate prevention suppresses events that are identical by contact_id, scope, channel, and consent_state within a 24-hour window, logging a dedupe_note instead of creating a new event. And Then identifier normalization is applied before dedupe: phone normalized to E.164; email lowercased, trimmed, and punycoded; names whitespace-collapsed.
Email Double Opt-In Flow
Given a web form submission includes email consent When the form is submitted Then the system sends a confirmation email with a unique tokenized link within 10 seconds and records a pending opt-in event with channel="email" and status="pending". And When the contact clicks the confirmation link within 30 days Then the ledger records a confirmed opt-in event referencing the pending event, status="confirmed"; otherwise the pending event expires and is not used for outreach. And Then the confirmation link is single-use, expires after 30 days, and logs IP and timestamp of confirmation; repeated clicks return an already-confirmed page without duplicating events.
Compliance Reporting & Audit Exports
"As a program director, I want compliance reports and audit exports per partner and timeframe so that we can demonstrate adherence and spot gaps."
Description

Deliver dashboards and exports that summarize consent coverage, revocations, expiries, and blocked actions by partner, campaign, channel, and time window. Provide a one-click audit pack for partners containing policy configuration snapshots, event summaries, and violation logs with reason codes. Integrate high-level metrics into the Impact Board to visualize compliance health alongside program outcomes, helping stewards spot gaps and prioritize outreach that aligns with actual permissions.

Acceptance Criteria
Partner Consent Coverage Dashboard
- Given a partner user with reporting permission and an org default timezone set, When they select a time window and optionally filter by campaign and channel, Then the dashboard displays for each channel: total_contacts, valid_consent_count, coverage_pct, unknown_consent_count, and the on-screen totals match the ledger within ±0.1%. - Given filters are applied, When exporting Coverage Summary (CSV), Then the file contains one row per partner-campaign-channel with fields [partner_id, partner_name, campaign_id, campaign_name, channel, total_contacts, valid_consent_count, coverage_pct, unknown_consent_count, window_start, window_end, generated_at] and values equal on-screen totals. - Given datasets up to 100k contacts, When loading the dashboard, Then median render time ≤ 2.5s and p95 ≤ 5s. - Given a user from Partner A, When viewing the dashboard, Then no records for other partners are visible in UI or exports.
Revocations and Expiry Trends by Channel
- Given a selected time window, When viewing Revocations & Expiries, Then weekly counts by channel for revocations and expiries are shown and sum to the exact total events in the ledger for that window. - Given the Upcoming Expiries filter set to 30/60/90 days, When applied, Then the list returns only contacts with consent_expiry_date within the selected horizon and with scope/channel details. - Given a point on the trend chart is clicked, When drilling down, Then a table of underlying events (contact_id, channel, scope, event_type, event_time, actor) appears with pagination and export. - Given org timezone is set, When computing windows, Then event bucketing respects org timezone. - Given a revocation reason is captured, When exporting Revocations (CSV), Then reason_code and reason_text columns are populated.
Blocked Actions Report with Reason Codes
- Given MergeGuard blocks an action, When the block occurs, Then a log entry is written with [timestamp, actor_id, action_type, entity_id, partner_id, campaign_id, channel, scope, reason_code, reason_detail] within 250ms of the event. - Given the Blocked Actions report is opened, When filters for date range, partner, campaign, channel, action_type, and reason_code are applied, Then results reflect filters and counts match the log table exactly. - Given the standardized reason catalog, When rendering rows, Then reason_code is one of the enumerated values and reason_detail is optional free text. - Given a non-admin for Partner A, When viewing the report, Then only Partner A’s blocked actions are visible and PII is limited to contact_id (hashed or internal) and not email/phone. - Given Export (CSV/JSON) is triggered, When download completes, Then file schemas match documentation and row counts equal visible results.
One-Click Audit Pack (ZIP) Generation
- Given a partner admin selects a time window ≤ 90 days, When clicking Generate Audit Pack, Then a ZIP is produced within 60 seconds containing: policy_config_snapshot.json, coverage_summary.csv, consent_events.csv, revocations.csv, expiries.csv, blocked_actions.csv, manifest.json, README.md. - Given the pack is generated, When validating manifest.json, Then it lists each file with byte size and SHA-256 checksum, and checksums validate. - Given the same inputs (partner, window, policies unchanged), When regenerating within 24 hours, Then produced files are byte-for-byte identical. - Given a completed pack, When sharing link is created, Then a pre-signed URL valid for 24h is issued and access is logged with [user_id, timestamp, ip]. - Given PII handling rules, When building CSVs, Then direct contact fields (email, phone) are redacted by default unless the requester has PII_Export permission.
Impact Board Compliance Metrics Integration
- Given the Impact Board is loaded, When compliance widgets render, Then they show: overall consent coverage % (all channels), blocked actions (last 7 days), revocations (last 30 days), upcoming expiries (next 30 days). - Given user taps a widget, When drill-through is invoked, Then the user is navigated to the corresponding detailed report with the same filters. - Given the board auto-refresh interval is 15 minutes, When data updates, Then widgets refresh without full page reload and show last_updated timestamp. - Given mobile viewport ≤ 414px, When rendering widgets, Then KPIs are legible, fit without horizontal scroll, and meet WCAG AA contrast. - Given tenant isolation, When a partner user views the board, Then metrics reflect only that partner’s data.
Policy Configuration Snapshot Versioning in Audit Pack
- Given policies have version IDs, When an audit pack is generated, Then policy_config_snapshot.json includes policy_version, effective_from, effective_to, consent_scopes per channel, merge/dedupe rules, and export restrictions. - Given policies changed during the selected window, When generating the snapshot, Then all versions effective in-window are included with their effective ranges. - Given the snapshot is parsed, When validating, Then the schema matches versioned documentation and timestamps are ISO 8601 with timezone offsets. - Given a reviewer compares two packs, When running a diff on policy_config_snapshot.json, Then differences in fields are explicit and limited to changed keys; unchanged sections are identical.

Scope Templates

Reusable, field-level sharing templates that mirror MOUs. Pick a partner and MergeGuard auto-applies redactions, allowed purposes, and expiration dates. Architects ship safe data shares in minutes, not meetings, with zero accidental PII leaks.

Requirements

Template Builder (Field-Level Rules Composer)
"As an org admin, I want to create reusable, field-level sharing templates that reflect each partner’s MOU so that staff and volunteers can share data safely without reinventing rules each time."
Description

A mobile-friendly interface to compose reusable Scope Templates that mirror partner MOUs at the field level. Admins select dataset fields, declare shareability, and attach rule actions (redact, mask, drop, aggregate) with allowed purposes and expiration windows. Supports template versioning, draft/publish states, partner-specific overrides, and validation against GiveCrew’s data dictionary. Stores rules as a signed, portable policy artifact consumable by MergeGuard. Enforces role-based permissions so only authorized users can create or modify templates, while organizers can apply approved templates during everyday workflows.

Acceptance Criteria
Mobile Field Selection and Rule Assignment
Given I am an Admin on a mobile device (viewport ≤ 414x896) viewing Template Builder for dataset X When I select 1–50 fields and set shareability per field with one action from [redact, mask, drop, aggregate] Then the chosen rule displays per field, touch targets are ≥44px, no horizontal scroll is required, and Save Draft is enabled only if ≥1 field has a rule And saving persists the configuration; reloading the builder restores identical selections and rules within 2 seconds over 3G Fast
Validation Against Data Dictionary and PII Safeguards
Given the data dictionary defines field types and PII tags for dataset X When I reference an unknown field or apply an incompatible action (e.g., aggregate on non-numeric without an aggregator) Then an inline error identifies the field and issue, and Save/Publish remain disabled until resolved And if a field is tagged PII and marked shareable without protection, Publish is blocked with a message requiring redact, mask, or drop And all validations run client-side and server-side; server rejects invalid payloads with 400 and error codes
Draft and Publish Workflow Controls
Given a template in Draft with name, description, at least one allowed purpose, and a valid expiration window When validation passes and I click Publish Then the template status becomes Published, fields/rules become read-only, and the template is available to organizer workflows and MergeGuard And Draft templates never appear in organizer workflows or MergeGuard selection lists
Template Versioning Behavior
Given a Published template version V When I create a new version and modify fields or rules Then the system assigns a new version identifier greater than V and marks the prior version immutable And the prior version remains available for existing shares, while the newest Published version is the default for new uses And attempting to edit a Published version directly is blocked; edits require creating a new version
Partner-Specific Overrides Within Constraints
Given a base Published template When I create a partner-specific override for Partner A Then I can only restrict sharing (add redactions, narrow allowed purposes, or shorten expiration) and cannot broaden beyond the base template And attempts to broaden rules are blocked with a clear error And the override inherits unchanged base rules and is labeled and filterable by Partner A
Signed Portable Policy Artifact Generation and Verification
Given I publish a template or partner override When the policy artifact is generated Then it serializes to the defined schema including template ID, version, dataset, field rules, allowed purposes, expiration, and partner ID (if any) And it includes a cryptographic signature and content hash; server logs the artifact ID And submitting the artifact to MergeGuard verification returns 200 with valid=true; any post-generation modification causes verification to fail
Role-Based Permissions and Application in Workflows
Given RBAC roles Admin and Organizer When an Admin accesses Template Builder Then they can create, edit, version, and publish templates And when an Organizer runs a sharing workflow, only Published (non-expired) templates are listed and selectable; Drafts and expired templates are hidden And unauthorized users attempting to create/modify templates receive 403 Forbidden and see no builder controls
Partner–MOU Mapping & Selection
"As a program lead, I want partner selections to automatically load the correct MOU terms and template so that data shares are compliant without manual cross-checking."
Description

A partner directory that links each organization to its governing MOU metadata, including effective dates, permitted purposes, data classes allowed, retention limits, and default Scope Template. When a user selects a partner during a share flow, the system auto-applies the mapped template and constraints. Supports MOU document attachment, renewal reminders, conflict detection (e.g., template allows field that MOU forbids), and region/program-level exceptions. Surfaces warnings for expired or missing MOUs and blocks shares that violate mandatory terms.

Acceptance Criteria
Partner MOU Metadata & Document Management
Given a partner "Org A" exists with MOU metadata fields (effective start date, effective end date, permitted purposes, allowed data classes, retention limit, default scope template) and an admin is editing the partner profile When the admin saves the profile with all required fields populated Then the values persist and are displayed in the partner profile exactly as entered And the same values are retrievable via API at GET /partners/{id}/mou And saving is blocked with a validation message if effective dates or default scope template are missing And the admin can upload a MOU document (PDF/DOC) and mark it as current And the uploaded document is stored, downloadable, and versioned; setting a new current version archives the prior version with its effective end date
Auto-Apply Scope Template on Partner Selection
Given a share flow is initiated and partner "Org A" with a mapped default scope template and valid MOU is selected When the user selects "Org A" as the recipient Then the mapped default scope template is auto-applied to the share configuration And allowed data fields are preselected and disallowed fields are excluded And redactions defined by the template are enforced (non-editable) And permitted purposes are restricted to those in the MOU; non-permitted purposes cannot be selected And the retention/expiration date is prefilled to the MOU retention limit And the UI displays a summary of applied constraints and the source MOU/template identifiers And the "Send Share" action is enabled only if the configuration remains compliant
Block Shares with Missing or Expired MOU
Given partner "Org B" has no MOU mapped or its MOU effective end date is before today When a user selects "Org B" during a share flow Then a blocking banner states "MOU missing or expired" and lists required remediation And the share configuration is read-only and the "Send Share" action is disabled And the API returns 409 Conflict with code MOU_REQUIRED if a send is attempted programmatically
Conflict Detection and Enforcement Against MOU
Given the default scope template for partner "Org C" includes fields or purposes not allowed by the partner's active MOU When the share flow loads or the user attempts to add a noncompliant field or purpose Then the system surfaces a warning that enumerates each conflict (field/purpose) and the violated MOU clause And noncompliant fields/purposes are automatically removed from the payload and cannot be re-added And the "Send Share" action remains disabled until the configuration is fully compliant
Region/Program-Level Exception Precedence
Given partner "Org D" has a global MOU, a region-specific MOU (Region=West), and a program-specific MOU (Program=Tutoring), all active today And a share is initiated with context Region=West and Program=Tutoring When the partner is selected Then the program-specific MOU is applied in preference to the region-specific, which is applied in preference to the global And the UI labels the applied MOU and shows the precedence rule used And if multiple MOUs exist at the same specificity level, the most recent effective start date is chosen deterministically
MOU Renewal Reminders Scheduling
Given an active MOU for partner "Org E" with effective end date D and owners assigned When the MOU is saved Then renewal reminders are scheduled at 60, 30, and 7 days before D to the owners and compliance recipients And if D is changed or the MOU is renewed, prior reminders are canceled and rescheduled to the new date And a reminder log shows scheduled/sent status and recipients
Retention Limit Enforcement and Auto-Expiry
Given a partner "Org F" MOU sets a retention limit of 90 days When a share is sent to "Org F" Then the share's expiration date is set to 90 days from send by default And the user cannot increase retention beyond 90 days; attempts are blocked with a validation message And the system schedules automatic revocation of access or data purge at expiration and displays the scheduled action in the share details And a reminder is sent 7 days before expiration to the share owner
MergeGuard Policy Enforcement Engine
"As a data steward, I want the system to enforce template rules automatically at share time so that accidental PII leaks are prevented even if a user makes a mistake."
Description

A deterministic, high-performance service that evaluates outgoing datasets against Scope Template rules at runtime. Applies redaction, masking, dropping, and aggregation; tags payloads with allowed purposes; and stamps access expiration metadata. Performs preflight checks, blocks disallowed fields, and logs decision traces for audit. Includes built-in PII detectors (names, emails, phone numbers, addresses, free-text scrubs) with configurable patterns and test harnesses. Operates in batch and streaming modes to support mobile uploads and scheduled syncs without leaking PII.

Acceptance Criteria
Preflight Blocks Disallowed Fields and Generates Report
Given a dataset containing fields outside the selected Scope Template and preflight mode enabled When the engine evaluates the dataset Then it returns a decision "BLOCK" without emitting any payload And the report enumerates all violating fields with the specific rule references And the report includes templateId, policyVersion, partnerId, requestId, and timestamp And the decision trace is persisted and queryable by requestId within 5 seconds And no data leaves the system boundary
Runtime Enforcement Applies Redaction, Masking, Drop, and Aggregation per Template
Given a Scope Template specifying redaction, masking, drop, and aggregation rules And an outgoing dataset containing matching fields and PII When the engine enforces at runtime Then redaction replaces values as configured by the rule And masking applies the configured mask format And dropped fields are omitted from the output schema And aggregation emits only allowed aggregate fields with correct values And the output contains zero disallowed fields or raw PII And a decision "ALLOW" is returned with a complete rulesApplied list
Allowed Purposes Tagging and Expiration Metadata Stamping
Given a Scope Template with allowedPurposes and an expiration interval When the engine produces an allowed payload Then the payload metadata contains allowedPurposes exactly as defined And an expiration timestamp in ISO 8601 UTC derived from evaluation time plus the interval And after the expiration timestamp, subsequent requests are blocked with decision "EXPIRED" And the trace records the computed expiration and purpose tags
Built-in PII Detectors Scrub Structured and Free-Text Fields
Given structured fields (name, email, phone, address) and free-text that contain PII And detector configuration includes default patterns and custom overrides When the engine evaluates the dataset Then all detected PII are masked or redacted per the template rules And no raw PII substrings from detected items appear in the output And detector hits are recorded in the decision trace with type and field/location And updating detector patterns takes effect within 60 seconds without restart And the provided test harness corpus passes with 100% of expected PII hits scrubbed
Decision Trace Logging and Audit Retrieval
Given any enforcement or preflight run When the decision completes Then a trace is written containing requestId, partnerId, templateId, policyVersion, decision, rulesApplied, fieldsModified, fieldsBlocked, detectorHits, allowedPurposes, expiration, and timestamps And the trace is retrievable via API by requestId and by time range And trace write failures cause the engine to fail closed (block) and return an error code And traces are retained for at least 365 days
Batch and Streaming Modes Meet Performance and Parity Guarantees
Given identical input and template When processed via streaming at sustained 100 RPS Then p95 end-to-end decision latency is <= 200 ms And when processed via batch of 100k records Then throughput is >= 10k records per minute with p95 per-record processing time <= 300 ms And decisions and outputs are byte-for-byte identical across modes
Deterministic, Idempotent Decisions Across Replays and Environments
Given the same input payload, Scope Template ID, and policyVersion When enforced multiple times with the same correlationId across retries and across staging and production Then the decision, output payload, and rulesApplied hash are identical And duplicate submissions with the same correlationId do not produce duplicate external emissions And the engine records a stable determinism checksum in the trace
Share Preview & Redaction Validation
"As a staff member preparing a share, I want to preview the redacted dataset so that I can verify it matches the MOU and contains no sensitive information."
Description

An interactive preview that shows exactly what a partner will receive after MergeGuard transforms the data. Highlights redacted, masked, or dropped fields; provides a side-by-side diff against the source; and flags rule conflicts or potential PII lingering in free-text. Supports sampling across records, downloadable test exports, and capture of reviewer sign-off for high-risk shares. Fully mobile-optimized so field staff can confirm compliance before dispatching a share link.

Acceptance Criteria
Mobile Share Preview Rendering
Given a logged-in user on a mobile device and a partner selected with a Scope Template When the user opens Share Preview Then the preview renders within 3 seconds for a 50-record random sample and displays the sample size And the layout fits 320–414 px widths without horizontal scrolling and record cards are swipeable And the header shows partner name, template name, allowed purposes, and expiration date And the user can change sample size to 5, 25, 50, or 100 and the preview refreshes within 2 seconds
Redaction, Mask, and Drop Highlighting
Given MergeGuard has transformed the dataset per the selected Scope Template When viewing any sampled record in preview Then REDACTED fields display the token “[REDACTED]” with red highlight, masked fields show masked format (e.g., ****1234) with yellow highlight, and dropped fields appear in a collapsible “Dropped Fields” panel And a legend explains each highlight style and all colors meet WCAG AA contrast And tapping a highlighted field reveals the governing rule name and rule ID
Side-by-Side Diff Against Source
Given the user has permission to view source records When the user toggles Side-by-Side Diff Then the left panel shows source values and the right panel shows transformed values aligned by field And changed fields are annotated with a diff badge and the per-record changed-field count equals the number of fields where source != transformed And dropped fields display a “Removed” placeholder on the transformed side And unchanged fields are not highlighted
Free-Text PII Detection and Warning
Given the dataset contains free-text fields When the preview loads Then potential PII (emails, phone numbers, SSNs, DOBs, postal addresses) is flagged inline and summarized in a banner with counts per type And each flagged instance shows a snippet and confidence score; only items with score >= 0.75 are flagged by default And the user can mark an item as false positive, which clears the flag for this share and writes an audit entry with user, timestamp, field, and snippet hash
Rule Conflict Detection and Blocking
Given rules from the Scope Template and any partner overrides are applied When a conflict exists (e.g., a field is both Required and Drop) Then a Conflict Panel lists each conflict with rule IDs, precedence applied, and a recommended resolution And the Share Link Dispatch action is disabled while any conflict remains unresolved And resolving a conflict updates the preview within 2 seconds and records the resolution in the audit log
Downloadable Test Export and Audit Trail
Given a preview is available When the user selects Download Test Export Then a CSV is generated containing only the current sample and the filename includes “-TEST” And exported columns and values exactly match the transformed preview; masked formats are preserved and dropped fields are absent And an audit entry records user, timestamp, partner, template, sample size, file SHA-256 checksum, and download outcome
Reviewer Sign-Off for High-Risk Shares
Given a share is classified as High Risk by policy When a reviewer opens the Sign-Off modal Then they must authenticate via SSO, check required attestations, and provide a typed signature with full name and role And the system stores signer, UTC timestamp, policy version, and preview hash and attaches a PDF receipt to the share record And Share Link Dispatch becomes enabled only after a valid sign-off is recorded
Expiration & Auto-Revocation Controls
"As an admin, I want shared access to automatically expire and be revocable according to the MOU so that partners can’t access data beyond the agreed period."
Description

Automated enforcement of share expiration dates derived from Scope Templates and MOUs. Generates time-bound links or scoped API tokens, auto-revokes access at expiration, and optionally purges or re-masks partner-held copies where supported. Sends renewal and sunset reminders to internal owners and partners. Supports manual early revocation, force-refresh of transformed datasets, and retroactive invalidation when an MOU is terminated. All actions are resilient to offline/mobile conditions and queued for reliable execution.

Acceptance Criteria
Auto-Expiration Revokes Link/Token Access
Given an active data share created from a Scope Template with expiration T When server time reaches T Then all associated time-bound links and scoped API tokens are invalidated within 60 seconds And subsequent partner requests receive HTTP 403 with error_code="share_expired" And an audit log entry is recorded with share_id, partner_id, revoked_by="system", reason="expiration" And the share status updates to "Expired"
Pre-Expiration Renewal and Sunset Reminders
Given a share with expiration T and designated internal owner and partner contacts When time is T-14d, T-7d, and T-1d Then renewal and sunset reminders are sent to both contacts via configured channels (email + in-app) And delivery status and timestamps are recorded per recipient And reminders include a one-click Renew action that opens a prefilled renewal flow And if the owner renews before T Then the expiration date updates and subsequent reminders are rescheduled to the new T And no further reminders are sent for the old T
Manual Early Revocation (Online/Offline Support)
Given a live share When the internal owner selects "Revoke now" Then the system invalidates tokens and links within 60 seconds and blocks access with HTTP 403 error_code="share_revoked" And all queued exports in progress for this share are canceled And the owner and partner receive immediate revocation notifications And an audit log entry is recorded with revoked_by="owner", reason="manual" Given the owner's device is offline when revocation is initiated When connectivity is restored Then the queued revoke executes exactly once using an idempotency key And the audit log preserves client_initiated_at and server_processed_at timestamps
MOU Termination Retroactively Invalidates Shares
Given an active MOU linked to one or more shares When the MOU status is changed to "Terminated" Then all shares derived from that MOU are revoked within 5 minutes And partner access attempts receive HTTP 403 with error_code="mou_terminated" And owners and partners are notified of the termination and revocations And the system prevents reactivation or renewal of these shares unless attached to a new active MOU And a consolidated audit record links the termination event to each revoked share
Force-Refresh of Transformed Datasets and Partner Purge
Given a share that delivers a transformed dataset with version Vn When the internal owner triggers "Force refresh" or the template's transformation rules are updated Then the dataset is regenerated as version Vn+1 using current rules And partners fetching after refresh receive only Vn+1 And any server-side cached artifacts for Vn are deleted And an audit entry records old_version, new_version, rule_set_id When the partner platform supports remote purge or re-mask Then the system issues purge/remask calls and marks status "Purge Confirmed" upon 2xx And otherwise marks "Purge Pending" and retries with exponential backoff for up to 24 hours And persistent failures raise alerts to the owner
Scoped Token/Link Issuance Enforces Template Limits
Given a Scope Template defining allowed fields, purposes, and an expiration policy (e.g., 90 days max) When an architect creates a share for a partner Then the system issues a link and/or scoped API token with embedded expiry <= policy And token scopes restrict access to only the allowed fields and endpoints And attempts to access out-of-scope resources return HTTP 403 with error_code="scope_violation" And the share metadata displays expiry date/time, scope, and partner identifiers And creation is denied with a clear error if requested expiry exceeds policy
Offline Queueing, Idempotency, and Conflict Handling
Given the user is offline on mobile and initiates a supported action (create share, renew, revoke, force-refresh) When connectivity resumes Then the action syncs within 30 seconds and is executed exactly once using an idempotency key And the UI transitions from Pending to Completed with server-confirmed timestamps And if a conflict exists (e.g., MOU terminated while offline) Then the server state prevails, the action is not applied, and the client displays "Conflict resolved: action not applied" And an audit entry records reason="conflict" with references to the blocking event
Audit Trails & Compliance Reporting
"As an executive director, I want comprehensive, exportable records of what was shared under which MOU so that I can prove compliance and build partner trust."
Description

End-to-end logging of template versions, partner selections, rule evaluations, redactions applied, expirations, and revocations with actor, timestamp, and dataset lineage. Provides exportable compliance reports for funders and boards, filters by partner, program, and timeframe, and integrates with the Impact Board to visualize safe-share activity. Detects anomalies (e.g., unusually broad field exposure) and raises alerts. Stores logs immutably with tamper-evident hashing and configurable retention aligned to policy.

Acceptance Criteria
Immutable Log Capture for Scope Template Events
Given a user creates, updates, selects a partner for, applies rules to, shares, or revokes a Scope Template When the action is committed Then an append-only log entry is written with: event_type, actor_id, actor_role, partner_id, program_id, template_id, template_version, dataset_id, dataset_lineage_id, rule_set_id, rule_evaluation_outcome, fields_shared[], fields_redacted[], purpose, expiration_at, revocation_reason, timestamp (ISO-8601 UTC), request_id, client_ip, and status (success/failure) And the API responds with 201 and returns entry_id and content_hash And attempts to modify or delete existing entries return 405 and are logged as security events And p95 log write latency is ≤ 200 ms at 100 RPS with idempotent writes via request_id
Compliance Report Export with Filters
Given audit logs exist across multiple partners, programs, and dates When a Compliance user filters by partner(s), program(s), event_type(s), and date range and selects Export CSV or Export PDF Then the export contains only matching rows with row count matching the on-screen total And CSV includes columns: timestamp, event_type, actor_role, partner, program, template_id, template_version, dataset_lineage_id, purpose, expiration_at, revocation_reason, fields_shared_count, fields_redacted_count, outcome, entry_id, content_hash And PDF includes the same data plus summary metrics (totals by event_type, partner, program) and a report_hash And exports complete ≤ 10 seconds for up to 1,000,000 rows or stream progressively with status updates And PII values are excluded/masked; only counts for fields_shared/redacted are shown And report metadata includes filter parameters, generated_by, generated_at (UTC), and anchor/verification status And access is restricted to roles Compliance Officer and above
Impact Board Safe-Share Activity Visualization
Given Impact Board is enabled for the organization When the user selects a timeframe and partner/program filters and toggles Safe Shares Then the board displays tiles/charts for: shares executed, unique partners, total fields redacted, and revocations with ≤ 15-minute data freshness And drill-down reveals the last 100 events with links to audit entries And counts match the compliance export for identical filters within 0.5% And users without permission to view audit data see no Safe Shares module
Anomaly Detection and Alerts for Broad Field Exposure
Given anomaly policies are configured (e.g., exposure_ratio > 0.7, inclusion of sensitive fields outside template, sudden partner scope expansion > 3× baseline) When a share evaluation meets any anomaly condition Then an alert is created with severity, rule_triggered, partner_id, template_id, dataset_lineage_id, and recommended actions And an in-app banner appears within 60 seconds and notifications are sent to configured channels (email/Slack) And the share is blocked unless a Compliance Admin submits an override with justification; override is logged And duplicate alerts for the same request_id within 15 minutes are deduplicated
Tamper-Evident Hash Chain and Verification
Given each log entry includes content_hash and prev_hash and daily anchors are published to external anchor storage When a verification job runs or a user requests verification for a time window Then the system validates the hash chain and anchor; returns VERIFIED or FAILED with first_bad_entry_id And exported reports embed anchor_id, verification_status, and verification_timestamp And nightly 1% random-sample verification completes successfully with 0 failures in healthy state And any verification failure raises a Critical alert within 5 minutes
Configurable Retention, Legal Hold, and Purge
Given per-program retention policies (e.g., 3 years) and legal hold can be applied at partner/program/case level When the retention period elapses and no legal hold is active Then eligible logs are purged within 24 hours by an automated job And a non-PII tombstone is retained with entry_id, purge_reason, and purge_timestamp; purge events are themselves logged And attempts to purge data under legal hold return 423 Locked and do not remove data And retention changes apply prospectively and are auditable
Secure Access to Audit Logs and Reports
Given RBAC is configured for audit viewing and export When an Organizer role user attempts to access audit logs or exports Then the request is denied with 403 and the attempt is logged And when a Compliance Officer views logs, PII values (e.g., field contents) are masked while metadata remains visible And successful access, filters applied, and exports by any user are logged with actor_id and timestamp

HashMatch Keys

Privacy-first matching that uses salted, rotating hashes of phone/email to find overlaps without exposing raw PII. Partners can reconcile joint pipelines confidently while keeping their original lists private.

Requirements

Salt Rotation Service
"As a data partnership admin, I want rotating, partner-specific salt management so that we can match records privately without enabling long-lived correlation."
Description

Centralized service that generates cryptographically secure, partner-scoped salts for phone and email keys, assigns versioned salt IDs, and rotates them on a configurable cadence. It enforces validity windows, maintains a limited overlap window for cross-version matching, and securely distributes active salt versions to authorized partners via a sealed channel. Salts are stored in an HSM-backed vault, never logged, and tracked with lifecycle metadata. The service triggers background rehash jobs where applicable, exposes read-only APIs for current/next salt versions, and guards against correlation by preventing reuse across partners and data types.

Acceptance Criteria
Scheduled Salt Rotation with Validity and Overlap Windows
Given partner P1 with data type "email" has rotation_cadence=30 days and overlap_window=24 hours and current active salt version v5 When the rotation time is reached Then version v6 is generated and becomes the active "current" at the scheduled time And versions v5 and v6 are both accepted for matching only during the 24-hour overlap_window And after the overlap_window ends, v5 transitions to Expired and is rejected by all APIs with HTTP 410 And there is no gap where neither v5 nor v6 is valid And lifecycle metadata records created_at, activated_at, expired_at in UTC for both versions
Cross-Version Matchability During Limited Overlap Window
Given the same phone number hashed with salt version v5 and with v6 for partner P1 When a match operation occurs during the configured 24-hour overlap_window Then the service returns a match (true) for v5-v6 comparisons And when the same comparison occurs after the overlap_window Then the service returns no match (false) And requests must specify partner_id and data_type; cross-partner or cross-type comparisons always return false
Sealed Distribution and Authorization for Active/Next Salts
Given an authorized partner client presenting valid mTLS credentials and scope token for partner P1 When it calls GET /salts/current?data_type=email Then the response is 200 with payload {version_id, valid_from, valid_to, salt_material} where salt_material is encrypted to P1's public key And all responses include Cache-Control: no-store and are sent over TLS1.2+ with mTLS And requests with missing/invalid certs or tokens are rejected with 401/403 And attempts to access another partner's salts return 403 and are audited
Cryptographically Secure, Partner- and Type-Scoped Salt Generation
Given the service generates a new salt for (partner_id=P1, data_type=phone) When the salt is created Then the salt entropy is >=256 bits and length >=32 bytes And the salt is produced by a FIPS-validated CSPRNG from the HSM And the (partner_id, data_type, version_id) is strictly monotonic increasing And the raw salt value is unique across all existing salts for all partners and data types (enforced by a global uniqueness constraint) And reuse of any prior salt value is rejected with a 409 and audit entry
HSM-Backed Storage with Zero Logging of Salt Material
Given creation and distribution of salts for any partner/data_type When system logs, traces, and audit streams are inspected Then no raw salt material or derived keys appear in any log line, trace attribute, or metric label And salts are stored only in the HSM-backed vault and never persisted in general application databases And attempts to read salt material outside the sealed API are denied and audited with subject, time, and outcome
Read-Only API for Current and Next Salt Versions
Given preannounce_window=6 hours for partner P1 data_type=email When calling GET /salts/next?data_type=email within 6 hours prior to rotation Then response is 200 with {version_id, valid_from, valid_to, checksum} and no salt_material until within 1 hour of activation And calls earlier than 6 hours return 204 No Content And GET /salts/current returns the single active version only and is idempotent and read-only And all responses include ETag headers and are rate-limited to 60 req/min per partner
Rotation-Triggered Background Rehash and Idempotent Processing
Given a rotation event for partner P1 data_type=phone from v5 to v6 When the rotation activates Then an event with {partner_id, data_type, from_version:v5, to_version:v6, activated_at} is published to the rehash topic within 5 seconds And downstream rehash jobs deduplicate on (partner_id, data_type, to_version) to ensure idempotency And 95% of rehash job batches complete within 2 hours; failures are retried up to 5 times with exponential backoff and are observable via metrics and audit logs
On-device PII Hashing SDK
"As a field organizer, I want phone and email hashed on my device before sync so that sensitive contact info is never exposed to servers or partners."
Description

Lightweight SDKs for iOS, Android, and web that normalize phone numbers and emails, apply partner-scoped salt plus app-held pepper, and compute stable hashes on-device so raw PII never leaves the user’s device. The SDK supports E.164 phone formatting and email canonicalization, constant-time hashing, memory scrubbing, offline queueing, and retries. It returns hash + salt_version only, integrates with GiveCrew’s mobile workflow, and includes a CLI for secure bulk hashing by staff. Telemetry is privacy-safe and excludes inputs.

Acceptance Criteria
Cross-Platform Deterministic Phone Hashing (E.164)
Given valid phone inputs in diverse formats and a default region When normalized by the SDK Then the normalized output is valid E.164 and matches libphonenumber expectations Given invalid phone inputs When normalization is attempted Then the SDK returns error code PHONE_INVALID and no hash is produced Given the same valid phone and partner_id with salt_version V When hashed on iOS, Android, Web, and CLI Then all hashes are identical and match the published test vectors Given a successful phone hash When the payload is returned or transmitted Then it contains only fields hash and salt_version and excludes raw and normalized phone values
Cross-Platform Deterministic Email Hashing (Canonicalized)
Given email inputs with mixed case, surrounding whitespace, and Unicode When canonicalized by the SDK Then domain is lowercased, surrounding whitespace is removed, Unicode is normalized (NFC), format is validated, and the canonical form is used for hashing Given malformed emails When canonicalization is attempted Then the SDK returns error code EMAIL_INVALID and no hash is produced Given the same canonical email and partner_id with salt_version V When hashed on iOS, Android, Web, and CLI Then all hashes are identical and match the published test vectors Given a successful email hash When the payload is returned or transmitted Then it contains only fields hash and salt_version and excludes raw and canonicalized email values
Mobile Workflow Integration Without PII Exfiltration (Network/Logs/Telemetry)
Given the GiveCrew mobile workflow triggers (signup, donation, shift assignment) When PII is entered and hashing is invoked Then the SDK is called on-device and only hash and salt_version flow to downstream services Given network traffic is captured during SDK operations When requests are inspected Then no raw or normalized phone/email values are present in any request payloads, headers, or query strings Given SDK logging in debug and release modes When logs are collected Then logs contain no raw/normalized inputs or hashes and redact sensitive fields Given telemetry is enabled When events are emitted Then events include only non-PII metadata (event_type, platform, durations, error codes, salt_version) and exclude inputs, normalized values, and hashes
Constant-Time Hashing and In-Memory Zeroization
Given 1000 random inputs of equal length per modality (phone/email) When hashing times are measured on reference devices Then p95/p50 <= 1.10 and timing is not correlated with input contents Given static and dynamic analysis of the hashing path When reviewing compiled artifacts Then there are no secret-dependent branches or memory access patterns Given heap/stack inspection within 100ms after hashing When inspecting process memory Then input buffers are zeroized and PII cannot be recovered via supported inspection APIs Given release builds When runtime flags are toggled Then zeroization cannot be disabled and remains enforced
Salt/Pepper Application and Salt Rotation Management
Given partner-scoped salt S(V) and app-held pepper P When hashing an input Then digest = H(canonical_input, S(V), P) and the response includes salt_version = V Given a rotation manifest promoting salt_version V+1 When the SDK receives it Then new hashes default to V+1 within 60 seconds and V remains usable during the configured grace period Given a pepper update When a process still holds the previous pepper Then the SDK emits PEPPER_VERSION_MISMATCH and refuses to mix peppers until restart Given storage inspection When checking persisted data and logs Then pepper material is stored only via OS secure storage (Keychain/Keystore/Web Crypto) and is never written to disk or logs in plaintext
Offline Queueing and Retry Resilience
Given the device is offline When a hash is produced Then the SDK enqueues a payload containing only hash and salt_version with minimal metadata and no PII Given queued items exist When connectivity resumes Then deliveries retry with exponential backoff (2s, 4s, 8s, ... up to 5m) for up to 10 attempts and preserve FIFO ordering Given app restarts or crashes When the app relaunches Then queued items persist and are delivered or expire after 7 days with status QUEUE_EXPIRED Given sustained high volume When the queue exceeds 10,000 items Then the SDK drops oldest items, emits QUEUE_OVERFLOW telemetry, and continues processing new items
Secure CLI Bulk Hashing for Staff Lists
Given a CSV/TSV input with columns id, phone, email and partner_id with salt_version When processed by the CLI Then the output contains only id, field, hash, salt_version and stdout/logs contain no raw inputs Given malformed rows When encountered during processing Then errors are written to stderr with id and error code, processing continues, and the process exits with code 2 if any row failed Given 100,000 valid rows on the reference machine When processed Then the CLI completes within 10 minutes and hashes match SDK outputs across platforms for the same inputs and salt_version Given temporary files and process memory When the job completes Then no temp files contain PII and in-memory input buffers are zeroized within 100ms after each batch
Partner Match Exchange API
"As a partner data engineer, I want a hardened API to exchange hashed datasets with metadata so that we can reconcile pipelines without ever sharing raw PII."
Description

Secure API and file exchange that accepts and returns only hashed identifiers and partner record tokens, with schemas that include salt_version, data type, and purpose tags. It uses OAuth2 with mTLS, request signing, rate limiting, and idempotent uploads via pre-signed URLs. Partners can submit hashed sets, receive overlap receipts, and pull match-result files scoped to an approved partnership. The API enforces retention windows, size limits, and purpose-policy checks, and emits webhooks for job status.

Acceptance Criteria
OAuth2 mTLS and Request Signing Enforcement
Given a partner client presents a valid OAuth2 access token, a trusted mTLS client certificate, and a correct request signature When calling any Partner Match Exchange API endpoint Then the request is authorized and returns 2xx Given an invalid or expired access token When calling any endpoint Then the response is 401 with error code invalid_token and no body data is returned Given a missing or untrusted client certificate When establishing the TLS session Then the connection is rejected before request processing Given a request with an invalid signature or mismatched signing key When calling any endpoint Then the response is 401 with error code invalid_signature and the event is audited Given a partner exceeds the configured rate limit When making further requests Then responses are 429 with a Retry-After header and X-RateLimit-* headers are present on limited and non-limited responses
Hashed Identifier Schema and PII Rejection
Given an upload payload contains only hashed identifiers and partner_record_token fields and includes salt_version, data_type, and purpose_tag per schema When the payload is validated Then the response is 202 Accepted with a job_id Given any raw email or phone value is detected When validating the payload Then the request is rejected with 422 Unprocessable Entity with error code pii_detected and no data is persisted Given a payload missing salt_version or with an unsupported data_type or purpose_tag When validating Then the request is rejected with 422 with field-level errors enumerating invalid fields Given hash values that do not conform to the schema's format constraints (e.g., length/charset) When validating Then the request is rejected with 422 and an example subset of offending records is returned up to a safe sample limit
Idempotent Uploads via Pre-signed URLs
Given a partner requests a pre-signed URL with a unique upload_id When uploading the same content multiple times with that upload_id Then only one ingestion job is created and subsequent attempts return the same receipt with status duplicate Given a subsequent upload uses the same upload_id but a different content checksum When uploading Then the request is rejected with 409 Conflict and no new job is created Given the pre-signed URL is used after its expiration time When uploading Then the response is 403 Forbidden and no data is ingested Given an upload exceeds the maximum allowed file size or record count When uploading Then the response is 413 Payload Too Large and the upload is not accepted Given a successful upload When completed Then a receipt containing upload_id, job_id, byte_count, record_count, salt_version, and received_at is retrievable
Job Status Webhooks with Verification and Retries
Given a partner has registered a webhook endpoint and verification key When a job changes state to accepted, processing, completed, or failed Then a webhook event is delivered with event_type, job_id, partnership_id, and a signed signature header Given the partner responds with a non-2xx code to a webhook delivery When delivering Then the system retries with exponential backoff for at least 24 hours or until a 2xx is received Given a webhook is delivered with an invalid signature When validating Then the event is rejected and a redelivery is attempted; the platform audit logs the failure Given the partner rotates webhook secrets When delivering Then subsequent events are signed with the new key after an activation timestamp
Overlap Receipt Retrieval Endpoint
Given a previously submitted job has completed matching When the partner calls GET /jobs/{job_id}/receipt Then the response is 200 and includes overlap_count, salt_version, data_type, purpose_tag, processing_started_at, processing_ended_at, and input_record_count Given a job_id that does not belong to the authenticated partnership When requesting the receipt Then the response is 403 Forbidden Given a job is still processing When requesting the receipt Then the response is 202 with status processing and no counts finalized
Scoped Match-Result File Access and Retention
Given a completed job with results and the caller is the authorized partner within the approved partnership scope When requesting the match-result file Then the response is 200 and the file contains only hashed identifiers and partner_record_token fields; no raw PII is present Given a caller from a different partnership or lacking scope When requesting the match-result file Then the response is 403 Forbidden Given the retention window for results has expired When requesting the match-result file or receipt Then the response is 404 Not Found and the file is deleted from storage; metadata shows expired=true
Purpose-Policy Enforcement and Audit
Given a submitted dataset declares a purpose_tag that is approved for the partnership When validating Then the job is accepted for processing Given a dataset declares a purpose_tag not approved for the partnership When validating Then the request is rejected with 403 Forbidden with error code purpose_not_allowed and no processing occurs Given any change to purpose policies or salt_version mappings When occurring Then the system records an immutable audit event including actor, timestamp, previous_value, and new_value
Hash Overlap Engine & Reports
"As a program director, I want accurate overlap counts and tokenized match lists so that we can coordinate outreach and measure joint impact without compromising privacy."
Description

Asynchronous matching engine that computes intersections across partner datasets by data type and salt_version, with optional cross-version grace windows to buffer rotations. It scales to tens of millions of keys using partitioned joins, generates deduplicated overlap lists keyed by partner tokens, and outputs privacy-preserving reports with counts, lift metrics, and thresholding to suppress small cells. The engine supports incremental re-runs, idempotency keys, backpressure, and exports via API and the Impact Board without exposing PII.

Acceptance Criteria
Same-Salt Overlap for Two Partners (Email)
Given partner A and partner B have uploaded hashed_email keys using salt_version "v6" with duplicates within each dataset And an idempotency_key "run-001" is provided When the engine executes the overlap job for data_type "email" and salt_version "v6" Then the output "overlap_list" contains only deduplicated partner_token pairs corresponding to keys present in both A and B And the total number of rows in "overlap_list" equals the set intersection size |A∩B| verified independently And no raw emails or unhashed values are persisted or emitted in outputs, logs, or metrics And all identifiers in outputs are partner_token values And the job record is stored with status transitions PENDING -> RUNNING -> SUCCEEDED and the provided idempotency_key
Cross-Version Grace Window Matching (Email v5↔v6)
Given partner A uses salt_version "v5" and partner B uses salt_version "v6" And a grace_window_days of 14 is configured and active for data_type "email" And keys rotated from v5 to v6 within the last 14 days exist When the engine runs the overlap job Then matches are emitted only for v5↔v6 pairs whose rotation timestamps fall within the grace window And no v5↔v6 matches are emitted when grace_window_days is set to 0 And the job audit report lists matched counts by version pair (v5-v5, v6-v6, v5-v6) and the applied window bounds And no raw PII is exposed in any report artifact
Privacy-Preserving Reports with Thresholding and Lift
Given a completed overlap job with counts per partner and a historical baseline from the immediately prior successful run When a privacy-preserving report is generated Then each cell shows total_records_A, total_records_B, overlap_count, and lift_vs_last_run_percent rounded to 1 decimal And any cell with overlap_count < 5 is suppressed and displayed as "Suppressed", with all derived metrics (including lift) suppressed as "—" And no field in the report includes raw PII or unhashed identifiers And the downloadable report artifact is signed, time-limited, and requires authorization mapped to the requesting partner
Incremental Re-run Processes Only Deltas (Idempotent)
Given a prior job "run-101" exists for partners A and B and data_type "phone", salt_version "v6" And only 2% of source partitions changed since "run-101" When a new run "run-102" is started with incremental=true Then only changed partitions are recomputed, unchanged partitions are reused from cache And the overlap_list and report artifacts for "run-102" are identical to a full recompute for the same inputs And re-submitting "run-102" with the same idempotency_key returns the existing artifacts without duplicating work And all operations are exactly-once from the perspective of downstream consumers
Scalable Partitioned Joins with Backpressure (10M+ Keys)
Given partners A and B each provide 25,000,000 hashed_phone keys with salt_version "v6" And the engine is configured with partition_size=1,000,000 and max_concurrency=20 When the overlap job runs Then the engine performs partitioned joins that never exceed the configured memory limits and no OOM events occur And backpressure engages when the task queue exceeds max_concurrency, preventing scheduler overload And all partitions complete with automatic retry for transient failures (up to 3 attempts) and no data loss And the end-to-end job completes within 4 hours at p95
Exports via API and Impact Board Without PII
Given a completed job and an authenticated partner user with scope "overlap.read" When the user calls GET /v1/overlaps?run_id=...&partner=... Then the API responds 200 with a JSON summary (counts, salt_version, data_type, run timestamps) and a signed URL to download overlap_list.csv And overlap_list.csv contains only partner_token_a, partner_token_b, data_type, salt_version and no raw PII And the Impact Board displays the same counts and lift as the API for that run within 1 minute of job completion And unauthorized requests return 403 and no information about the existence of the run is leaked
Robust Validation and Failure Isolation
Given a submission where partner B's dataset declares salt_version "v7" not whitelisted and includes malformed hashed_email rows When a job is created Then validation fails before compute with error code "INVALID_INPUT" and details per offending field, with zero PII echoed And no overlap artifacts are produced for the invalid partner, and no partial intersections with that partner are exposed to others And metrics and logs capture the failure with correlation_id and without PII And upon resubmission with corrected inputs, only the previously failed partitions are processed
Consent & Compliance Logging
"As a compliance officer, I want comprehensive audit logs and policy enforcement around matching so that we meet regulatory obligations and partner agreements."
Description

End-to-end audit trail that records who initiated each match, the lawful basis and purpose, partner identities, dataset descriptors, salt_version, timestamps, and outcomes in an immutable append-only log. The system provides searchable audits, export for compliance reviews, automated policy checks to block disallowed purposes, and retention aligned to agreements and regulations. DSAR workflows can locate and purge hashed records via source record references without re-identifying PII.

Acceptance Criteria
Append-Only Log Entry on Match Initiation
Given a user or system initiates a partner match operation When the match request is submitted Then the system writes an append-only log entry containing: log_id, correlation_id, initiator_id, initiator_role, partner_ids, dataset_descriptors, lawful_basis, purpose_code, policy_version, salt_version, request_timestamp (UTC ISO-8601), processing_node_id, and outcome (queued/succeeded/blocked/failed) And the entry includes prev_hash and record_hash to provide tamper-evident integrity And the entry persists within 1 second and remains durable across node restarts And the entry is immutable to all roles; edit/delete attempts are rejected with HTTP 403 and are themselves logged
Automated Policy Check Blocks Disallowed Purpose
Given policy configuration disallows purpose_code X for partner pair Y at policy_version Z When a match is attempted with purpose_code X between partner pair Y Then the system denies the operation before any hash comparison occurs And returns HTTP 403 with error_code=POLICY_BLOCK and policy_version=Z And logs an outcome=blocked entry with policy_reference and no hash material recorded And the denial is completed in p95 <= 500 ms
Searchable Audit by Multiple Fields
Given ≥100,000 log entries exist When an AuditViewer searches by any combination of initiator_id, partner_id, purpose_code, lawful_basis, dataset_descriptor, salt_version, correlation_id, outcome, and time range Then the system returns matching results ordered by request_timestamp desc with total_count and pagination cursors And p95 query latency <= 3 s, p99 <= 5 s for up to 10 combined filters And result rows contain no raw PII and only permitted fields per schema_version And access is denied (HTTP 403) to users without AuditViewer or higher
Compliance Export for Review
Given an Auditor requests an export for a time range and partner scope in CSV or JSON When the export job is submitted Then the system generates a file containing all required audit fields plus integrity_proof (e.g., Merkle root or signature) and schema_version And excludes any raw PII while preserving necessary hashed or reference identifiers And completes within 60 s for up to 50,000 rows using streaming; larger jobs are chunked and notify on completion And the export action is logged with file_id, requester_id, and filters used And the file is access-controlled, encrypted at rest, and auto-expires after 7 days
Retention Enforcement and Purge
Given retention policies are configured per partner and purpose (e.g., 365 days) When records reach their retention threshold or an agreement is terminated Then a purge job marks affected entries by appending redaction_tombstone records with log_id, purge_reason, policy_reference, requestor_id, timestamp, prev_hash And underlying stored record bodies and hash artifacts are securely erased within 24 hours while preserving the tombstone chain And API access to purged content returns HTTP 410 Gone And p95 purge completion time <= 12 hours for 1,000,000 eligible records And purge metrics and failures are visible to admins
DSAR Locate and Purge by Source Record Reference
Given a validated DSAR is received with source_record_reference(s) When DSAR processing is initiated Then the system resolves all related audit entries across all salt_versions via the reference index without re-identifying PII And presents counts and items for review to a Data Administrator And upon approval, appends redaction_tombstones and securely erases stored hash artifacts for the selected entries And produces a DSAR completion report with evidence of actions and timestamps within 7 days And DSAR states (received/needs_info/approved/denied/completed) are tracked and auditable
Salt Rotation Capture and Backward Compatibility
Given a salt rotation event is executed When new matches are performed after the effective_timestamp Then each log entry records the incremented salt_version And matches do not compare hashes across differing salt_versions unless an explicit back-compat window is enabled by policy with an end date And DSAR locate resolves entries across all salt_versions for the same source_record_reference And search and export support filtering by salt_version And the rotation event is itself logged with rotation_id, effective_timestamp, and policy_version
Admin Match Rules Console
"As an org admin, I want a clear console to manage partners and match rules so that our team can safely operate HashMatch without developer intervention."
Description

Administrative UI to configure partnerships, authorized users, data types to match (phone, email), rotation cadence, privacy thresholds, and purpose policies. It surfaces current and next salt_version, last rotation event, API credentials, webhook endpoints, and recent job statuses. The console offers dry-run validation of sample hashes, downloadable schema definitions, and role-based access controls, with all actions audited. It integrates with GiveCrew’s existing org and Impact Board for unified visibility.

Acceptance Criteria
Create and Save Partnership Configuration
Given an authenticated Org Admin with "Match Admin" role When they create a new partnership with partner_name, data_types (subset of {phone, email}), purpose_policy, privacy_threshold, webhook_endpoint, and api_credential_name populated Then the console validates required fields, enforces allowed values, and blocks save with inline errors for any invalid input And on success the configuration is saved, appears in the partnerships list within 2 seconds, and an audit event "PARTNERSHIP_CREATED" is recorded with actor_id, partner_name, and redacted secrets
Configure Rotation Cadence and View Salt Versions
Given an existing partnership selected in the console When the admin sets rotation_cadence to a valid cron schedule and saves Then the console renders "Next rotation" in UTC and displays current_salt_version, next_salt_version, and last_rotation_event (timestamp and job_id) And the change is logged as "CADENCE_UPDATED" with old_value and new_value
Manage API Credentials and Webhook Endpoints
Given an Org Admin with "Match Admin" role When they view API credentials in the console Then credential values are masked by default (show last 4), copy-to-clipboard is available, and full reveal requires "View Secrets" permission and re-auth within the last 5 minutes When they rotate an API credential Then a new credential is generated and shown once, the old credential is revoked within 60 seconds, and "API_KEY_ROTATED" is logged When they update webhook_endpoint and run Test Delivery Then a signed POST is sent, the UI displays HTTP status code and latency, and the console refuses to save if a 2xx response is not received
Role-Based Access and Action Auditing
Given a user without "Match Admin" or "Match Viewer" roles When they attempt to access the Admin Match Rules Console Then access is denied with HTTP 403 and no configuration data is rendered Given a user with "Match Viewer" role When they load the console Then they can view configurations but cannot create, update, rotate, or delete, and all secrets remain masked And every create, update, rotate, test, or delete action writes an immutable audit record with actor_id, action, target_id, timestamp, and IP, excluding secret values
Dry-Run Validation of Sample Hashes
Given a Match Admin opens Dry-Run Validation for a partnership When they select data_type and salt_version and input a sample hash in hex or base64 Then the console validates format, rejects raw phone or email patterns, and prevents submission until input is valid When they submit the sample hash Then the system returns pass or fail with reason within 2 seconds, does not persist the sample value, and logs "DRY_RUN_VALIDATED" with metadata (data_type, salt_version, length) only
Surface Recent Job Statuses and Impact Board Visibility
Given recent match jobs exist for the organization When the admin opens the console Jobs panel Then the last 20 jobs are listed with status (Queued, Running, Succeeded, Failed), partner, start_time, end_time, duration, and job_id, with filter by status and partner And when a job completes, the Impact Board updates an aggregate "Matches reconciled" metric within 5 minutes and links back to the job detail, without exposing PII And failed jobs display error class and correlation_id, and users with "Match Admin" can trigger a retry once per job
Configure and Enforce Privacy Thresholds
Given an organization policy minimum_threshold is configured When an admin attempts to set a partnership privacy_threshold below the minimum Then the save is blocked with the message "Threshold must be greater than or equal to the organization minimum" And when a dry-run or job result would disclose counts below the effective threshold Then the UI and API return masked or zeroed aggregates and log "THRESHOLD_ENFORCED" for the partnership

Merge Preview

A side-by-side record preview with risk flags, attribution carryover, and a “result by partner view.” One click shows exactly what each org will see post-merge. Captains and Stewards merge fast with no surprises—and instant undo if needed.

Requirements

Side-by-Side Merge Preview
"As a Steward, I want to compare duplicate records side-by-side so that I can confidently choose the correct values and merge quickly on mobile."
Description

Present two candidate records in a split-screen interface with field-level diffs, conflict highlighting, and per-field selection controls to choose the source-of-truth. Render a live result panel that updates as selections change, reflecting the final merged record exactly as it will be saved. Support GiveCrew entities including contact profiles, donations, volunteer hours, shift assignments, tags, households, and notes. Provide mobile-optimized layout, sticky headers for key identifiers, keyboard shortcuts, and accessibility-compliant focus states. Persist draft selections while navigating the queue. On apply, perform an atomic merge and display a confirmation with the resulting record ID.

Acceptance Criteria
Side-by-Side Field Diffs with Conflict Highlighting
Given two candidate records are selected for merge When the preview loads Then both records are displayed side-by-side with identical field ordering and labels Given a field has different values across the two records When the field row renders Then the row is visually highlighted as a conflict and both values are visible simultaneously Given a field has identical values across both records When the field row renders Then the row is marked as non-conflict and is not highlighted
Per-Field Source Selection Controls
Given any field row in the merge preview When displayed Then left and right source selection controls are present, enabled (unless read-only), and operable by mouse, touch, and keyboard Given the user selects a source for a field When the selection is made Then the chosen source is clearly indicated, the alternate is de-emphasized, and the choice is persisted in the draft state Given a field is read-only or cannot be overridden When the row renders Then selection controls are disabled and an explanatory tooltip or help text is available
Live Result Panel Reflects Final Merged Record
Given the live result panel is visible When the user changes any per-field selection Then the result panel updates within 200 ms to reflect the exact merged value for that field Given associated collections (donations, volunteer hours, shift assignments, tags, households, notes) exist on either record When the preview renders Then the result panel shows the combined set with de-duplication by unique identifier and accurate counts Given the user clicks Apply Merge When the save completes successfully Then the persisted merged record exactly matches the result panel snapshot at the time of click
Entity Coverage: Contacts, Donations, Hours, Shifts, Tags, Households, Notes
Given a merge pair includes any supported entity data (contact profiles, donations, volunteer hours, shift assignments, tags, households, notes) When opened in the merge preview Then those fields and collections render with field-level diffs and per-field or per-item selection controls where applicable Given donations, hours, or shifts appear on both records with the same unique identifier When displayed in the result panel Then they are shown once (de-duplicated) and retain their original identifiers Given tags or household memberships exist on either record When displayed in the result panel Then the combined set reflects union minus duplicates, preserving existing relationships
Mobile Layout with Sticky Identifiers
Given a viewport width of 320–420 px When the merge preview loads Then the UI adapts to a mobile-optimized layout with no horizontal scrolling and all controls reachable by vertical scroll Given the user scrolls the preview on any device When key identifiers are in view Then the primary identifiers (name, primary email/phone, record IDs) remain pinned as sticky headers on both sides of the preview Given touch input on mobile When interacting with selection controls Then tap targets are at least 44x44 px and respond within 100 ms
Accessibility and Keyboard Shortcuts
Given keyboard-only navigation When traversing the merge preview Then Tab and Shift+Tab move focus in a logical order across all actionable elements without focus traps, and each focused element shows a visible focus indicator with at least 3:1 contrast (WCAG 2.1 AA) Given a screen reader user When focus lands on a field row Then the assistive tech announces the field name, both candidate values, conflict status, and current selection with appropriate roles and states (name, role, value) Given the user invokes shortcuts When pressing Up/Down Then focus moves to the previous/next field row; when pressing Left/Right, the left/right value is selected for the focused row; when pressing Enter/Space, the current selection is confirmed; when pressing ? the shortcuts help is displayed
Draft Persistence, Atomic Merge, and Confirmation
Given the user makes selection changes for a merge pair When navigating to another pair in the merge queue and returning within the same session Then the prior selections for that pair are restored within 1 second Given the user clicks Apply Merge When any error occurs during save Then no partial changes are committed (atomicity), the draft selections remain intact, and a clear error is shown with a retry option Given a successful merge When the operation completes Then a confirmation appears within 1 second displaying the resulting record ID and a link to open the merged record
Risk Flags and Conflict Scoring
"As a Captain, I want clear risk flags and a simple score so that I can avoid bad merges and escalate edge cases."
Description

Detect and surface merge risks with rule-based flags and an aggregate risk score shown prominently above the preview. Rules include conflicting primary contact info, divergent legal names, do-not-contact or consent mismatches, different partner ownership, overlapping active shift assignments, donation ledger discrepancies, household linkage conflicts, and age-sensitive data differences. Classify flags by severity and provide inline explanations with suggested actions. Block merges for critical violations, require justification for major warnings, and record acknowledgments for audit. Make rules configurable by org policy and local compliance settings.

Acceptance Criteria
Aggregate Risk Score Visible and Responsive
Given a potential merge is loaded in Merge Preview And risk rules are enabled per the organization's current configuration When the preview loads Then an aggregate numeric risk score and severity label are displayed above the preview And the score equals the sum of the weights of all currently triggered rules And the severity label maps to the configured threshold ranges When any field that affects a rule is modified in the preview Then the score, severity, and list of triggered flags update within 1 second of the change
Severity Classification with Inline Explanations and Suggested Actions
Given one or more risk flags are triggered When the user selects a flag in the panel Then the flag shows its severity (Critical, Major, Minor, Info) And an inline explanation describes the rule logic and references the implicated fields/records And a suggested action is displayed (e.g., Verify legal name, Obtain consent, Reassign ownership) And a control is available to resolve, justify, or acknowledge the flag without leaving the preview
Critical Violations Block Merge Action
Given at least one Critical-severity flag is present When the user attempts to confirm the merge Then the merge action is blocked and a message lists the critical violations And the Confirm Merge control is disabled And no override or justification path is offered And the user can navigate to resolve the underlying conflicts from the message
Major Warnings Require Justification
Given there are Major-severity flags present and no Critical flags When the user attempts to confirm the merge Then the UI requires the user to enter a justification note for each unresolved Major flag And each justification must be at least 10 characters And the merge proceeds only after all required justifications are entered And each justification is stored with user ID and timestamp
Acknowledgment and Audit Logging of Flag Handling
Given any merge with one or more flags (Critical, Major, Minor, or Info) When the user clears, justifies, or acknowledges a flag Then an immutable audit entry is recorded capturing: flag rule, severity, explanation snapshot, action taken (cleared/justified/acknowledged/blocked), user ID, timestamp, and pre/post values of affected fields where applicable And audit entries are visible in the record's Audit History view And audit entries cannot be edited or deleted via the UI When only Minor or Info flags remain Then the user may proceed after acknowledging them, and acknowledgments are recorded in the audit
Configurable Rules and Thresholds with Compliance Constraints
Given an Org Admin opens Risk Rules settings When the admin enables or disables a rule, changes its severity or weight, or updates severity thresholds Then the changes save successfully and take effect for new Merge Preview sessions within 5 seconds When the admin attempts to disable or downgrade a rule locked by local compliance (e.g., consent mismatch) Then a validation error explains the constraint and the change is rejected And all configuration changes are logged with user ID, timestamp, and a diff of changes
Rule Coverage and Correct Flagging Across Conflict Types
Given test records exhibiting each of the specified conflict types: - conflicting primary contact info - divergent legal names - do-not-contact or consent mismatches - different partner ownership - overlapping active shift assignments - donation ledger discrepancies - household linkage conflicts - age-sensitive data differences When each case is reviewed in Merge Preview Then the corresponding rule triggers exactly one flag per distinct conflict, with the configured severity And the inline explanation references the specific conflicting fields/records and sources And no flag is triggered for a clean control case with consistent data
Attribution Carryover and Result by Partner View
"As a Partner Admin, I want to preview post-merge attribution by partner so that my organization’s credits, rosters, and totals remain accurate."
Description

Ensure all attributions and relationships carry over correctly on merge, including donation credit, volunteer hours, shift assignments, tags, communications history, and credit splits across partners or chapters. Provide a "Result by Partner" view that renders, for each partner organization, the exact post-merge counts, totals, rosters, and field visibility they will see, including deltas from pre-merge state. Recalculate Impact Board metrics and maintain links for receipts and reminders without breaking historical references. Support granular credit-splitting rules and preserve source attribution for reporting. Display any attribution changes as part of the preview before commit.

Acceptance Criteria
Donation Credit and Attribution Carryover on Merge
Given Record A has donations: $100 (100% Partner Alpha, source=SMS) and $50 (60% Alpha, 40% Beta), and Record B has $200 (100% Beta, source=Email) When the user previews a merge with A as target Then the preview shows post-merge partner donation totals: Alpha $130, Beta $120, and donor lifetime total $350 And donation transaction IDs remain unchanged and will be re-parented to the merged person on commit And per-transaction source attribution (campaign, channel, solicitor) remains intact for reporting When the merge is committed Then partner donation reports reflect the same totals within 1 minute And no duplicate donation records or receipts are created
Volunteer Hours, Shifts, Tags, and Communications History Preservation
Given Record A has 6 volunteer hours and is assigned to Shift S1 (checked in), and Record B has 4 volunteer hours and is also assigned to Shift S1 (not checked in) and Shift S2 When the user previews a merge with A as target Then the preview shows post-merge hours total = 10 and a single assignment for S1 (preserving A's check-in) and assignment for S2 And tags from both records are unioned with de-duplication And communications history from both records is combined chronologically with original sender, channel, timestamps, and message IDs preserved When the merge is committed Then the merged record reflects the same hours, shift assignments, tags, and communications as previewed And no orphaned or duplicate shift assignments or messages exist
Result by Partner View: Post-Merge Totals, Rosters, Field Visibility, and Deltas
Given Partners Alpha and Beta have different field visibility rules (e.g., Beta cannot see personal phone) And Records A and B have partner-specific credits and roster memberships When the user opens Result by Partner in Merge Preview and toggles between Alpha and Beta Then for each partner the view shows exactly the post-merge totals (donations, hours), counts, and rosters that partner will see, with restricted fields hidden per policy And a delta panel shows per-partner changes from each record’s pre-merge state (adds, removals, and net value changes) And the numbers and visibility in this view match those seen by that partner after commit
Granular Credit Splits Across Partners/Chapters with Source Attribution
Given donations and volunteer hours on Records A and B include credit splits across Partners Alpha, Beta, and Chapter C with two-decimal precision When previewing a merge Then all existing splits are preserved and normalized so that per-item splits sum to 100.00% And invalid or incomplete splits are flagged with an error and the merge action is blocked until corrected And source attribution fields (solicitor, acquisition channel, campaign) remain associated to each item and are available in partner/chapter reports post-merge When the merge is committed Then partner and chapter rollups reflect split-weighted totals identical to the preview
Impact Board Recalculation and Historical Receipt/Reminder Links Integrity
Given the organization’s Impact Board is driven by donations, hours, and shifts, and both records have existing receipts and scheduled reminders with resolvable links When the merge is committed Then the Impact Board recalculates and displays updated totals and posters within 30 seconds And previously sent receipt and reminder links continue to resolve (HTTP 200) and point to the merged record context without breaking historical content And scheduled reminders are de-duplicated so that only one reminder per intended schedule is active for the merged person
Attribution Change Preview Before Commit
Given records have overlapping and conflicting attributions (e.g., differing partner credits, tags, and solicitors) When the user views the Merge Preview Then an Attribution Changes panel lists all changes that will occur by partner and category (adds, removals, re-allocations) with before/after values and per-partner deltas And high-impact changes (e.g., net negative credit to a partner) are flagged And the user must explicitly confirm these changes before the Commit action is enabled And the committed result matches the previewed changes
Permission-Aware Visibility Preview
"As a Chapter Captain, I want to see exactly what users with different roles will see after the merge so that I can verify data sharing boundaries are respected."
Description

Simulate role- and org-scoped visibility with a one-click switch to preview the merged record as seen by specific roles (Captain, Steward, Partner Admin, Volunteer Lead) and partner entities. Apply existing RBAC and data-sharing agreements to the preview, masking restricted fields and related records accordingly. Indicate fields that will change visibility post-merge and explain why. Prevent merges that would create unauthorized exposure based on current policies. Cache and reuse permission evaluations for responsiveness on mobile while ensuring correctness at commit time.

Acceptance Criteria
One-Click Role and Partner Preview Switch
Given I am on the Merge Preview for a candidate record And the Preview As control lists roles Captain, Steward, Partner Admin, and Volunteer Lead and available partner entities When I select a role and a partner entity Then the preview updates to display only fields and related records permitted by current RBAC and data-sharing agreements for that role+partner And restricted fields are replaced with the placeholder "Restricted" And restricted related records are omitted with an "N items hidden" indicator And the header displays "Previewing as [Role] @ [Partner]" And the Result by partner view lists each partner with accurate per-partner visibility for the selected role
Visibility Change Indicators and Reasons
Given a proposed merge between Record A and Record B and a selected role+partner When the visibility preview is rendered Then every field and related record whose visibility will change post-merge for that role+partner is annotated with a badge of "Newly visible" or "Newly hidden" And selecting the badge reveals a reason panel citing the specific RBAC rule and data-sharing agreement identifiers that drive the change And the panel includes the evaluation basis (subject, resource, action, condition) without revealing hidden values And a summary chip shows total counts of visibility increases and decreases for the current role+partner
Unauthorized Exposure Merge Block
Given the system detects that post-merge, at least one role+partner would gain access to data not authorized by current policies When the user attempts to commit the merge Then the merge is blocked before commit And an error modal lists the affected role(s), partner(s), and the specific fields/related records that would be exposed, with links to policy references And the user is offered navigation to the relevant permission-aware previews for each affected combination And the block event is recorded in the audit log with user, timestamp, policy versions, and affected resources
Mobile Permission Caching and Responsiveness
Given I am previewing a merge on a mobile device over 4G And no policy or record changes have occurred since the last preview of the same record version When I switch between roles or partner entities for the same record version within the session Then subsequent previews are served from cache and render within ≤250 ms at p95 And the first uncached evaluation renders within ≤800 ms at p95 And the cache key includes record revision, selected role, selected partner And cache TTL is ≤5 minutes and is immediately invalidated on policy updates or record changes And cached results exactly match server-evaluated permissions for the same key
Commit-Time Permission Revalidation
Given a user has reviewed a permission-aware preview that may have used cached evaluations When the user taps Merge Then the system recomputes permission evaluations server-side against the latest policies and record state And if the recomputation differs from the preview or would cause unauthorized exposure, the merge is halted and the user is prompted to refresh the preview with a diff of changes And only when recomputation matches the preview and no exposure is detected does the merge proceed And the commit stores an audit snapshot including policy versions, evaluation timestamp, role+partner context reviewed, and record revision
Masking Explainability and Audit Trail
Given a field or related record is hidden or masked in the permission-aware preview When the user selects "Why hidden?" Then the system displays the governing RBAC rule ID and data-sharing agreement reference and the evaluation path (subject, resource, action, condition outcome) And the explanation is available offline if previously fetched, with a visible "Offline – may be stale" notice And on reconnect the explanation refreshes automatically And the explanation never reveals the masked values themselves And viewing of explanations is logged with user, timestamp, and field identifiers
Instant Undo and Safe Rollback
"As a Steward, I want an instant undo after merging so that I can reverse mistakes without waiting for IT."
Description

Offer a time-bounded instant undo that fully reverts a completed merge, restoring the original records and all related attributions, links, and IDs. Capture a pre-merge snapshot of both records and their relationships and perform merges within transactional boundaries to ensure atomicity and idempotent rollback. Provide a visible undo affordance immediately after merge and an entry point from the merged record’s activity log within the allowed window. Validate that no conflicting downstream edits occurred before rollback and surface a guided resolution if they did.

Acceptance Criteria
Immediate Undo From Merge Confirmation Banner
Given a user completes a merge in Merge Preview and is authorized to undo merges Then a visible "Undo Merge" affordance appears in the success banner immediately and remains enabled until the org-configured undo window expires And a complete pre-merge snapshot of both records, their relationships, attributions, links, and external IDs was captured before the merge commit When the user clicks Undo within the allowed window and confirms Then the system restores the two original records and all captured relationships, attributions, links, and external IDs from the snapshot And the merged record is deactivated or removed per policy with references pointing to the restored records And the UI navigates to the restored primary record and shows a success message And the audit log records the undo with actor, timestamp, and correlation ID
Undo Via Activity Log Within Allowed Window
Given a merge has completed and created a merged record activity entry Then the activity log shows an "Undo Merge" action available to authorized users during the undo window When the user initiates undo from the activity log Then a confirmation dialog summarizes the changes to be reverted and displays time remaining When the user confirms within the window Then rollback executes with the same outcomes as the banner undo (state, navigation, audit) When the window has expired Then the action is disabled and displays an "Undo window expired" message and a timestamp
Conflict Detection and Guided Resolution Before Rollback
Given downstream edits occurred to the merged record or its related entities after the merge time When an undo is initiated Then the system compares the pre-merge snapshot to the current state to detect conflicting changes (field updates, relationship changes, new attributions or links) And if conflicts are found, rollback is blocked and a guided resolution screen lists each conflict with options: discard post-merge edits and proceed, or cancel undo When the user chooses discard-and-proceed Then rollback executes and a detailed conflict resolution entry is added to the audit log When no conflicts are found Then rollback proceeds automatically
Atomic, Idempotent Rollback Execution
Given an undo request is submitted for a recent merge When the rollback process runs Then all changes are applied within a single transaction to guarantee atomicity And if any step fails, the transaction is rolled back and no partial changes persist And repeated or duplicate undo requests result in a single rollback effect and return an "Already undone" response without side effects And concurrent merges or rollbacks targeting the same records are prevented with a clear error
Restoration of Attributions, Links, and External IDs
Given the pre-merge snapshot includes donation attributions, volunteer shift assignments, partner visibility ("result by partner view"), risk flags, relationship links, and external IDs When rollback completes successfully Then each listed element is restored exactly to the snapshot state And counts, totals, and partner-facing views match their pre-merge values with no duplication or loss And risk flags and warnings reappear exactly as they were pre-merge
Time Window Governance and Expiration Behavior
Given an org-configured undo window is defined Then the undo affordances are available only within that window and hidden or disabled after expiration When an undo is attempted after expiration Then the system refuses the operation, displays an "Undo window expired" message with the merge timestamp, and records the attempt in the audit log And a read-only activity log entry remains accessible that shows the merge details and window expiration
Merge Audit Trail and Reporting
"As a Compliance Lead, I want a detailed audit trail of merges so that we can meet reporting requirements and resolve disputes."
Description

Maintain an immutable audit log for every merge, including actor identity, timestamp, source records, pre- and post- values by field, risk flags encountered, override justifications, attribution adjustments, and any permission simulations performed. Provide searchable, filterable views in-app and export to CSV for compliance. Expose summary metrics (merges by user, average risk score, undo rate) and link audit entries to affected donations, shifts, and contacts. Enforce retention policies aligned with org compliance requirements.

Acceptance Criteria
Audit Entry Completeness for Merge
Given a captain confirms a merge in Merge Preview that includes risk flags, override justifications, permission simulation(s), and attribution adjustments Then a single audit entry is appended with: merge_id, event_type=merge_commit, actor_user_id, actor_role, actor_org_id, timestamp_utc (ISO 8601, ms), source_record_ids, affected_entity_ids (contacts/donations/shifts), field_deltas [{field_name, pre_value, post_value}], risk_flags [{flag_id, score, label}], override_justification (text), attribution_adjustments [{entity_id, from_value, to_value, rationale}], permission_simulations [{simulation_id, viewer_org_id, result_preview_hash}], data_version, content_hash, previous_hash, and undo_ref=null And the audit entry is retrievable via API/UI within 5 seconds of merge confirmation When an undo of that merge is performed Then a new audit entry with event_type=merge_undo is appended, references the original merge_id, sets undo_ref bidirectionally on both entries, and records restored field_deltas And both entries appear in search results for either affected entity
Immutability and Tamper Detection
Given any existing audit entry When any user or system attempts to modify or delete it via UI or public API Then the request is rejected with HTTP 403 and error_code=immutable_audit_log and no changes are persisted And audit entries are stored append-only with a hash chain (content_hash, previous_hash) When the integrity verification job runs nightly Then 100% of entries validate their hash chain; any discrepancy marks the entry as tampered=true and raises an alert event within 5 minutes And the audit detail view displays integrity_status as Verified or Tampered for every entry
In-App Search and Filter
Given at least 100,000 audit entries across 12 months When a Steward filters by date range, actor_user_id, actor_org_id, risk_score range, has_override (true/false), has_undo (true/false), and entity_id Then the result set reflects all applied filters accurately And queries returning ≤5,000 rows load in ≤2 seconds; queries returning ≤50,000 rows load in ≤5 seconds When sorting by timestamp or risk_score ascending/descending Then results are correctly ordered on every page And pagination supports page sizes of 25, 50, 100 with consistent total counts When searching override_justification text for a keyword Then only matching entries are returned with the keyword highlighted in results
CSV Export for Compliance
Given a filtered audit result set with N rows (N ≤ 50,000) When the user selects Export CSV Then a RFC4180-compliant UTF-8 CSV downloads within 60 seconds and contains exactly N data rows plus a single header row And the header columns are: merge_id,event_type,actor_user_id,actor_role,actor_org_id,timestamp_utc,source_record_ids,affected_entity_ids,field_deltas_json,risk_flags_json,override_justification,attribution_adjustments_json,permission_simulations_json,undo_ref,data_version,content_hash,previous_hash And all timestamps are UTC ISO 8601 with Z and millisecond precision; lists/objects are JSON-encoded in their cells When the exporting user lacks PII export permissions Then PII within pre_value/post_value (email, phone, address) is masked per policy and a pii_redaction column is set to true on those rows And the exported row count and a SHA-256 checksum are displayed post-download
Summary Metrics Dashboard
Given audit data exists for the selected time window When the user opens Reporting → Merge Metrics Then the dashboard shows: merges_by_user (top 10 users with counts), average_risk_score (mean of risk_flags scores per merge), undo_rate = undo_events / merge_commits And changing the time window (Last 7/30/90 days, Custom) recomputes metrics in ≤2 seconds When filtering by actor_org_id or actor_user_id Then all displayed metrics and charts reflect the filters And clicking a metric segment (e.g., a user bar) drills down to the corresponding filtered audit list with identical counts And metric totals match an equivalent raw audit query exactly
Entity Linking From Audit and Entities
Given an audit entry lists affected_entity_ids for donations, shifts, and contacts When the user clicks any affected entity id Then they are navigated to that entity’s detail page if they have permission; otherwise an insufficient_permissions notice is shown When viewing an entity’s History tab Then all merge-related audit entries impacting that entity are listed with timestamp, actor, event_type, and a deep link back to the audit detail And if an entity has been redacted or deleted by retention policy Then the link displays Redacted with purge_reason and no navigation occurs
Retention Policy Enforcement and Legal Hold
Given an org-wide audit retention policy of 24 months and no legal hold When the nightly retention job runs Then audit entries older than 24 months are purged per policy: either PII fields within entries are redacted or the entry is deleted and replaced by a tombstone containing event_type, timestamp_utc, org_id, purge_reason, content_hash, previous_hash And a purge_summary entry is appended to the audit log with counts of redacted vs deleted records When a legal hold is applied to the org or a case tag Then matching entries are excluded from purge until the hold is removed, and all hold changes are logged with actor, timestamp, and scope And the Retention Report screen predicts the next purge batch counts and, after execution, actuals match predictions ±1%
Fast Merge Queue and Shortcuts
"As a Volunteer Coordinator, I want a fast queue with shortcuts so that I can clear duplicate backlogs during shift breaks."
Description

Provide a queue of suspected duplicates with preloaded previews for rapid processing, optimized for mobile and low-connectivity environments. Support keyboard-first navigation, one-tap merge, batch actions, and background application of non-blocked merges. Show progress, errors, and retry options without losing context. Accept inputs from internal dedupe scans and external APIs, de-duplicate the queue itself, and throttle to avoid server overload. Persist user preferences for field order and default choices to speed decisions while honoring risk rules.

Acceptance Criteria
Preloaded Merge Preview on Queue Navigation
Given the user has a queue loaded with at least 20 suspected duplicate pairs and prefetch is enabled When they move to the next item via keyboard or tap Then the side-by-side merge preview renders with risk flags and attribution carryover within 300 ms P90 on mobile (throttled) and 150 ms P90 on desktop And the result-by-partner subview is available within the preview without additional network calls And preview content reflects the latest server state at time of fetch (validated via ETag/Last-Modified)
Keyboard-First Navigation and One-Tap Merge
Given focus is on the queue list When the user presses ArrowUp/ArrowDown or J/K Then selection moves to the previous/next item with a visible focus indicator When the user presses M or Enter on a non-blocked item Then a merge is submitted immediately and the next item is selected And shortcut actions execute within 100 ms P90 locally and dispatch a network request within 200 ms P90 When a merge is blocked by risk rules Then a confirmation dialog explains the blocking reason and keyboard focus lands on the primary action
Batch Non-Blocked Merges in Background with Throttle
Given the user multi-selects 2–100 non-blocked items in the queue When they activate Merge in Background Then merges are enqueued client-side and processed with a maximum concurrency of 3 requests per user And on HTTP 429/503 responses, retries use exponential backoff (initial 1s, factor 2, max 30s) honoring Retry-After And the UI remains navigable; users can continue reviewing other items while processing occurs And upon success items are removed from the queue; upon failure items are marked Error with a Retry action
Queue Ingestion and De-duplication Across Sources
Given internal dedupe scan results and external API candidate pairs are available When the queue is built Then pairs are normalized to a canonical key and duplicates across sources are collapsed into one queue item And the highest risk score is retained and source badges indicate Internal, External, or Both And no two items in the queue represent the same entity pair And the default sort is descending risk score, then recency
Mobile and Low-Connectivity Optimization
Given a mobile device on simulated 3G (400 ms RTT, 1 Mbps down, 750 kbps up) When the user opens the queue Then the first 20 items with skeleton previews appear within 3 seconds P90 and prefetch begins for the next 10 items And if connectivity drops mid-session, prefetched previews remain viewable and actions are deferred with an offline banner And average payload per item (excluding images) is ≤ 5 KB via compression
Progress, Error Handling, and Retry Without Losing Context
Given one or more merges are in-flight When progress updates occur Then a persistent indicator shows counts for queued, processing, succeeded, and failed When a merge fails Then an inline error displays code and message and provides a one-tap Retry that reuses the original payload and correlation ID And after error or retry the user’s scroll position, selection, and current filters are preserved
Persisted User Preferences and Risk Rule Compliance
Given a user reorders fields in the preview and sets default choices for tie-breakers When they return on any device Then those preferences load from the server profile and apply within 200 ms P90 after authentication And risk rules take precedence; any default that violates a risk rule is ignored and the field is highlighted with the reason And the user can reset to system defaults, clearing stored preferences and immediately updating the current session

Attribution Shield

Locks and visibly carries “brought-by” credit and partner source on every merge, including future updates and exports. Scorecards stay fair, partners keep recognition, and turf conflicts go down.

Requirements

Immutable Attribution Fields
"As a volunteer coordinator, I want each record to retain its brought-by and partner source without being overwritten so that recognition remains fair and disputes are minimized."
Description

Introduce dedicated, tamper-evident attribution fields on core records (Person, Donation, Signup, ShiftAssignment) including brought_by_user, partner_source, and an ordered attribution_chain. These fields are set at creation and thereafter locked from manual edit or bulk overwrite, persisting through all CRUD operations, imports, mobile flows, and API events. Implement write-once semantics with controlled, reason-coded admin overrides, plus full audit logging (who, what, when, why) for any attribution write. Enforce referential integrity so attribution remains valid when referenced partners/users are merged or deactivated. Benefit: preserves fair credit, reduces disputes, and ensures consistent recognition across the system.

Acceptance Criteria
Write-Once Attribution on Record Creation
Given a new [Person|Donation|Signup|ShiftAssignment] is created via web UI When the payload includes brought_by_user and/or partner_source Then the saved record stores these values exactly as submitted and locks them And attribution_chain is initialized in order: [brought_by_user (if provided), partner_source (if provided)] with no duplicates And an audit entry is recorded capturing who, what fields, values, when, and why=create Given a new record is created via API POST or mobile flow When attribution fields are provided Then the fields are persisted once, attribution_chain is initialized as above, and an audit entry is written Given a record has just been created with attribution set When any user attempts to edit attribution fields in the UI immediately after creation Then the inputs are read-only and the change is prevented with a visible notice "Attribution is write-once" And no change is saved
Attribution Integrity Across Bulk Import
Given an import job processes rows that match existing records (by external_id or dedupe rules) When incoming rows contain attribution fields differing from stored values Then stored attribution fields remain unchanged And the import result reports skipped_attribution_overwrite count for those rows And an audit log records a blocked attempt with actor=job, who initiated, when, and why=blocked_write_once Given an import job creates brand-new records When attribution fields are present Then the fields are set on create, attribution_chain is initialized in order, and an audit entry is written per record Given an import row references a non-existent brought_by_user or partner_source When the job runs Then the row fails with 422 Unprocessable Entity and a clear message indicating the missing reference And no partial record is created
API and Mobile Updates Respect Write-Once Lock
Given an existing record with attribution set When a client calls API PATCH/PUT with a different brought_by_user, partner_source, or attribution_chain Then the API responds 403 Forbidden with error_code=ATTR_WRITE_ONCE_LOCK and no change is persisted And an audit event of type=blocked_write is recorded with actor and request_id Given a mobile user edits a record offline and changes attribution fields When the device syncs Then the server rejects the attribution changes, returns a conflict explaining write-once policy And the mobile client restores server values for those fields and marks the local attempt as failed
Admin Override with Reason Codes and Full Audit
Given a user has role=System Admin and permission=ATTR_OVERRIDE When they initiate an attribution override via the dedicated Override flow Then they must select a reason_code from the allowed list and enter a free-text note (min 10 chars) And upon confirm, the system updates the specified attribution field(s) and re-seals them And an audit entry is created with who, old_value, new_value, when, reason_code, note, and channel=override Given a user without ATTR_OVERRIDE permission When they attempt an override Then the action is blocked with 403 Forbidden and no change occurs Given a record has an override When it is viewed in UI or via API GET Then the response includes a tamper_evident indicator and a link/reference to the audit trail
Merges Preserve and Surface Attribution
Given two Person records are merged into a survivor When their attribution fields differ Then survivor.brought_by_user and survivor.partner_source remain the survivor's values if present; otherwise they adopt the non-null values from the merged-into record And survivor.attribution_chain becomes the stable ordered union of both chains with duplicates removed, preserving original write timestamps order And an audit entry records the merge details and resulting attribution Given Donation/Signup/ShiftAssignment records are merged or re-parented as part of dedupe When attribution is present on both Then the same preservation and ordered-union rules apply at the child-record level And no manual edits are introduced during merge
Referential Integrity for Attribution References
Given a record's brought_by_user references an active user When that user is deactivated Then the attribution reference remains valid (points to the deactivated user), and reads expose the user status=Deactivated without nulling the reference Given partner_source A is merged into partner_source B When attribution references A Then references (including within attribution_chain) are updated to B atomically, with an audit entry of the remap Given an attempt is made to delete a user or partner that is referenced by any attribution field When the deletion is requested Then the system blocks the deletion with 409 Conflict and returns the count of dependent records
Exports and Reporting Carry Immutable Attribution
Given a user exports People, Donations, Signups, or ShiftAssignments to CSV When the export completes Then the file includes brought_by_user_id, brought_by_user_name, partner_source_id, partner_source_name, and attribution_chain columns that match stored values exactly Given scorecards or partner recognition reports are generated When attribution exists Then credit is computed solely from immutable attribution fields and matches what appears on Profile and record detail views Given downstream systems pull records via API GET/list endpoints When they retrieve records Then attribution fields are always present in the payload and are read-only in the API schema/documentation
Merge-Safe Attribution Logic
"As a data manager, I want merges to preserve and clearly resolve attribution so that partner scorecards stay accurate after deduping."
Description

Define deterministic merge rules that preserve and consolidate attribution when duplicates are merged. On Person/Org merge, keep the earliest non-null brought_by, union and deduplicate partner sources, and maintain an ordered attribution_chain with a clearly marked primary source. Propagate final attribution to dependent objects (Donations, Signups, ShiftAssignments). Provide a merge preview that visibly shows resulting attribution and flags conflicts. Ensure repeatable outcomes on future merges/unmerges and background dedupe jobs. Benefit: ensures attribution survives de-duplication and keeps scorecards accurate.

Acceptance Criteria
Preserve earliest non-null brought_by on merge
Given two or more Person/Org records with various brought_by values and created_at timestamps When they are merged into a single survivor record Then survivor.brought_by equals the earliest non-null brought_by by created_at ascending And if created_at timestamps are identical, the brought_by from the record with the lowest stable record_id is used And if all brought_by values are null, survivor.brought_by remains null
Union and deduplicate partner_sources across duplicates
Given duplicate Person/Org records where partner_sources includes overlapping and unique sources with first_seen_at timestamps When they are merged Then survivor.partner_sources equals the de-duplicated union of all partner_sources And ordering is deterministic by first_seen_at ascending, then by source_id ascending And the count of survivor.partner_sources equals the number of unique sources across inputs
Build ordered attribution_chain with clearly marked primary
Given merged records with brought_by and partner_sources When the merge is committed Then survivor.attribution_chain is created containing the primary source at index 0 and all other sources ordered by first_seen_at ascending, then source_id ascending And survivor.attribution_primary equals survivor.brought_by And each chain element stores source_id, source_type, contributed_by_record_id, and first_seen_at
Propagate final attribution to Donations, Signups, and ShiftAssignments
Given Donations, Signups, and ShiftAssignments linked to any of the merged records When the merge completes Then each dependent object's attribution.brought_by equals survivor.brought_by And each dependent object's attribution.partner_sources equals survivor.partner_sources And the number of updated dependent objects equals the count of objects previously linked to any merged record And updates are idempotent; re-running the merge produces no additional changes
Merge preview shows resolved attribution and conflict flags match post-merge
Given selection of records to merge in the UI When the merge preview is displayed Then the preview shows the resulting survivor.brought_by value, with the contributing record identified And conflicting non-null brought_by values are flagged with a message indicating the earliest non-null rule and the chosen winner And the preview shows the de-duplicated union of partner_sources and the ordered attribution_chain with primary marked And after confirming the merge, the committed survivor fields match the preview exactly
Deterministic outcomes across repeated merges and background dedupe
Given the same set and values of duplicate records When merged multiple times in different input orders and via background dedupe jobs Then the survivor.brought_by, partner_sources, and attribution_chain are identical across all runs And merging three or more records yields the same outcome regardless of pairwise grouping strategy And exports of survivor and dependent objects include the same attribution values across runs
Unmerge restores prior attribution and dependent object state
Given a completed merge When an unmerge is performed Then the original records' brought_by and partner_sources are restored to their pre-merge values And dependent objects revert to their pre-merge attribution assignments And a subsequent re-merge of the same records reproduces the same survivor attribution values as before
Attribution in Exports and Reports
"As a partnerships lead, I want exports and scorecards to include attribution fields so that partners receive visible, verifiable credit in reports."
Description

Extend all exports (CSV/XLS) and reporting surfaces to carry attribution consistently. Add brought_by, partner_source(s), and primary_source indicators to export schemas with clear headers and documentation. Update the Impact Board and partner scorecards to filter and aggregate metrics by partner and brought-by, with date ranges and drill-down to underlying records. Maintain attribution fields through downstream integrations and ensure permission-aware masking in exports. Benefit: partners receive transparent credit in external deliverables and progress posters without manual reconciliation.

Acceptance Criteria
CSV/XLS exports include attribution columns with defined headers and formats
Given an organization user with Export permission When the user exports Contacts, Donations, and Shifts to CSV and XLS Then each file contains columns named exactly: brought_by, partner_sources, primary_source And partner_sources values are pipe-delimited (|), de-duplicated, and alphabetically sorted And primary_source is a boolean string of true or false for every row And brought_by is populated for all records with a known attribution; otherwise the cell is empty And the three columns appear in the schema documentation referenced from the export modal
Impact Board filters and aggregates by partner and brought-by with date ranges and drill-down
Given records exist across multiple partners and brought_by values And the Impact Board is loaded with a date range selector When the user applies Partner filter = "Partner A" and Date Range = last 30 days Then all visible totals and charts reflect only records whose partner_sources includes "Partner A" within that date range And clicking any metric tile opens a drill-down list of the exact underlying records with columns brought_by, partner_sources, primary_source And the count of records in the drill-down equals the metric total (±0) And p95 time to render the filtered board is ≤ 3 seconds for up to 100k records
Partner Scorecard aggregates accurately and supports brought-by filter with drill-down
Given a Partner Scorecard is opened for "Partner B" And a date range of Quarter-to-Date is selected When the user adds a Brought-By filter for a specific organizer Then all KPIs (e.g., signups, confirmed shifts, donations and amount) are computed from records where partner_sources includes "Partner B" and brought_by equals the selected organizer within the date range And selecting a KPI shows a drill-down list of underlying records with attribution columns And exporting the drill-down produces a CSV whose totals match the KPI values (±0)
Attribution is preserved across merges and reflected in all future exports
Given two duplicate contacts with different partner_sources and a single brought_by are merged per Attribution Shield rules When the merge is completed Then the resulting contact retains a union of partner_sources and the original brought_by is locked and unchanged And any future edits to non-attribution fields do not alter brought_by or remove existing partner_sources And the next CSV/XLS export shows the preserved brought_by and unioned partner_sources for the merged record And an audit log entry records the merge and the preserved attribution values
Permission-aware masking in exports and reports
Given a user without the View Attribution permission When they view the Impact Board or Partner Scorecards Then aggregated metrics exclude masked attribution from any dimension they are not permitted to view and do not leak disallowed values When the same user exports data Then the export includes the columns brought_by, partner_sources, primary_source with all values replaced by the literal REDACTED And an export audit log records that attribution masking was applied And a user with the permission sees full, unmasked values in both UI and exports
Downstream integrations carry attribution fields and respect permissions
Given an active downstream integration that syncs records via API or scheduled sync When a sync runs Then the payload for each record includes fields brought_by, partner_sources (pipe-delimited), and primary_source (true/false) And a consumer receiving the payload can parse partner_sources into an array by splitting on | And if the integration credential lacks View Attribution permission, the three fields are present with values REDACTED And sync success metrics report the count of records sent and masked
Export schema documentation and in-app help reflect attribution fields
Given the user opens the Export Schema documentation from the export modal When the page loads Then it documents brought_by, partner_sources, primary_source with definitions, examples, data types, and masking behavior And examples show partner_sources as pipe-delimited values (e.g., Partner A|Partner B) And the documentation shows Last Updated date of today or later and version number matching the current app release And inline tooltips in the export modal summarize each field and link to the full docs
UI Attribution Badges & Filters
"As a field organizer, I want visible attribution badges and filters in lists and profiles so that I can target follow-ups and give proper credit during daily work."
Description

Display clear, non-editable attribution badges across the app: profile headers, list rows, activity feeds, signups, shift assignments, and donation details. Provide hover/tooltips with full attribution_chain and timestamps. Add quick filters and saved views by brought_by and partner_source on People, Donations, and Shifts. Surface a subtle lock icon indicating fields are protected. On mobile, render badges compactly with accessible color contrast. Benefit: makes attribution visible in daily workflows and supports rapid filtering without exporting to spreadsheets.

Acceptance Criteria
Badges on profile headers, list rows, and activity feeds (web)
Given a person profile with non-null brought_by and partner_source, when viewing the profile header on web, then two non-editable attribution badges labeled "Brought by: <name>" and "Partner: <name>" are visible and each shows a lock icon. Given a list view (People, Donations, Shifts) with rows containing attributed records, when the list renders, then each attributed row shows compact badges for brought_by and partner_source and rows without attribution show no badges. Given an activity feed item that references an attributed record, when the item renders, then the same compact badges appear in the item metadata and are non-interactive except for tooltip/focus.
Tooltip shows full attribution chain with timestamps (web and mobile)
Given an attribution badge, when hovered with a pointer or focused via keyboard on web, then a tooltip opens showing the full attribution_chain in order with each node's display_name, role/type, and timestamp in the user's locale. Given an attribution badge on mobile, when tapped or long-pressed, then a modal/tooltip opens showing the same details and can be dismissed by tapping outside or the close control. Given an attribution_chain longer than five nodes, when the tooltip opens, then the panel is vertically scrollable and pressing Escape (web) or Back (mobile) closes it. Given a timestamp in the chain, when displayed, then it includes date and time with timezone abbreviation (e.g., 2025-08-09 14:32 PT).
Quick filters by brought_by and partner_source on People, Donations, and Shifts
Given the People, Donations, or Shifts list on web or mobile, when the user opens quick filters, then brought_by and partner_source filters are available as selectable fields with typeahead search of known values. Given one or more values selected for brought_by or partner_source, when applied, then the results include only records matching any selected values within the same field (OR) and matching across both fields when both are set (AND). Given an applied quick filter, when the page is refreshed or the URL is shared, then the same filter state is restored from the URL/query string. Given active filters, when the user clears them, then the full unfiltered dataset is shown and filter chips are removed.
Saved views by attribution on People, Donations, and Shifts
Given attribution filters applied on People, Donations, or Shifts, when the user saves as a named view, then the view appears under Saved Views for that entity with the exact filter state. Given a saved view, when re-opened in a new session, then the previously saved attribution filters are reapplied and the results match the original save (given unchanged data). Given a saved view, when renamed or deleted, then the Saved Views list updates immediately and the change persists across sessions.
Mobile compact badges with accessible contrast and interaction
Given the mobile app in light or dark mode, when attribution badges render, then the text-to-background contrast meets WCAG AA (>= 4.5:1). Given a small screen (<= 360dp width), when badges render, then each badge truncates overflow with ellipsis and the full value is available in the tooltip/modal on tap. Given a user on mobile, when interacting with a badge, then the tappable area is at least 44x44dp, is focusable, and a visible focus indicator is shown; screen readers announce "Brought by: <name>, locked" or "Partner: <name>, locked".
Lock icon and edit prevention for protected attribution fields
Given any screen showing brought_by or partner_source, when the user attempts to click into or edit those fields, then inline edit controls are disabled or absent and a tooltip on the lock icon states "Protected by Attribution Shield". Given a form that includes attribution fields, when the form loads, then those inputs are read-only and attempts to change them are prevented by the UI; saving does not alter their values and an error or inline notice informs "Attribution fields are locked". Given the lock icon next to the field label or badge, when hovered or focused, then it has an aria-label "Attribution field is locked" and the explanatory tooltip appears.
Badges on signups, shift assignments, and donation details (web)
Given a signup detail page for a person with attribution, when viewed, then badges for brought_by and partner_source are displayed in the header area with lock icons. Given a shift assignment roster and shift detail page, when rendered for attributed participants, then each assignment row displays compact badges and the participant card/header shows the badges. Given a donation detail view for an attributed donation, when viewed, then badges are displayed adjacent to the donor name and are non-editable with lock icons.
Attribution Permissions & Audit Governance
"As an admin, I want strict controls and a full audit trail on attribution changes so that credit cannot be altered without accountability."
Description

Introduce role-based controls and policies governing who can set or override attribution. Restrict overrides to designated Admin roles with mandatory reason codes and optional partner approval workflows. Generate audit logs and change history diffs, with notifications to partner owners on any override. Provide periodic integrity reports highlighting records with missing or conflicting attribution, and a dashboard widget showing recent attribution changes. Benefit: prevents unauthorized edits, increases trust, and provides traceability for compliance and partner relations.

Acceptance Criteria
Admin-Only Attribution Edit Enforcement
Given a signed-in user whose role is not Admin When the user attempts to create, edit, or remove any attribution fields (brought-by, partner source, partner scorecard linkage) Then the system blocks the action, shows an authorization error with code ATTRIB_403, and no data is changed And the blocked attempt is recorded as a security event with userId, role, recordId, attemptedFields, IP, and UTC timestamp Given a signed-in Admin When the Admin opens the attribution editor for a record Then the editor loads successfully and displays the current immutable attribution state with an Override option
Mandatory Reason Codes for Overrides
Given an Admin initiates an attribution override on a record When no reason code is selected from the configured list Then the Save/Override action is disabled and inline validation indicates a required reason code Given an Admin selects a reason code and (optionally) enters notes up to 500 characters When Save/Override is clicked Then the override is persisted with reasonCode and notes, and the form confirms success And overrides cannot be saved without a reason code under any entry path (UI, API, import) Given the reason code list is modified in settings When an Admin opens the override form Then only active reason codes are available for selection
Optional Partner Approval Workflow
Given org settings require partner approval for overrides affecting specified partners or record types When an Admin submits an attribution override Then an approval request is created and the record’s attribution status is set to Pending, leaving the current attribution unchanged And designated partner owner(s) are assigned as approvers and can Approve or Reject via in-app action or secure email link When an approver Approves Then the override is applied, audit-linked to the approval, and the request status becomes Approved with approverId and UTC timestamp When an approver Rejects Then no changes are applied and the request status becomes Rejected with a required rejection reason If no action occurs within the configured SLA window Then the request expires, notifies the initiating Admin, and no automatic changes are applied
Audit Log and Change History Diffs
Given any attribution change is completed (direct override or approved workflow) When the change is committed Then an immutable audit entry is written capturing recordId, actorId, actorRole, channel (UI/API/Import), reasonCode, approvalRequestId (if any), and a before/after diff of attribution fields with UTC timestamp And audit entries are viewable to Admins in the record’s History tab and exportable as CSV with consistent headers And audit entries are read-only via UI and API and retained for a minimum of 1 year or per tenant retention policy, whichever is longer Given a user views the history for a record When two versions are selected Then a diff view highlights exactly which attribution fields changed and their previous and new values
Partner Owner Notifications on Overrides
Given any attribution override is initiated (whether approval is required or not) When the request is created Then partner owner(s) receive an in-app notification immediately and an email within 1 minute containing record reference, proposed change summary, reason code, and initiating Admin When the override is Approved, Rejected, or Completed Then the initiating Admin and partner owner(s) receive a status update notification And users may opt out of email but not in-app notifications; quiet hours preferences defer email but queue in-app alerts And notification delivery failures are retried up to 3 attempts with exponential backoff and logged
Periodic Attribution Integrity Report
Given the integrity checker schedule is enabled When the weekly job runs at 02:00 local org time by default Then a report is generated that identifies: (a) missing attribution (null/empty brought-by or partner source on records created after feature enable date), (b) conflicting attribution (multiple active partner sources on a single record), and (c) mismatched attribution (partner source inconsistent with brought-by mapping rules) And the report provides totals per category and a CSV with recordId, detectedIssue, detectedOn, and suggestedFix And the report is delivered in-app and via email to Admin distribution list; downloading requires Admin role And all report entries link to one-click fix or review actions where applicable
Recent Attribution Changes Dashboard Widget
Given a dashboard includes the Recent Attribution Changes widget When the widget loads Then it displays changes from the past 7 days by default with columns: recordId, old→new partner, actor, reason code, and UTC timestamp And it supports filtering by partner, actor, reason code, and time range, and paginates 50 items per page And clicking an item opens the record’s history view focused on the selected change And the widget auto-refreshes at least every 5 minutes and shows last refresh time And displayed data respects role-based access controls, hiding records outside the viewer’s permitted partners
Signed Source Tokens & API Support
"As a campaign partner, I want secure share links and API params that auto-assign attribution so that new signups and donations are correctly credited without manual effort."
Description

Enable signed, tamper-resistant source tokens for forms, share links, and API submissions that encode partner and advocate IDs. Validate tokens server-side to prevent spoofing and map them to attribution fields at record creation. Provide endpoints for partners to generate time-bound tokens, plus webhooks to notify when attributed records are created. Support idempotency keys to avoid duplicate attributions on retries. Benefit: ensures accurate auto-attribution from campaigns and integrations while reducing manual tagging.

Acceptance Criteria
Partner Generates Time-Bound Source Token via API
Given a partner authenticated with valid API credentials When they POST to /v1/attribution/tokens with partnerId, optional advocateId, channel in [form, link, api], and ttlSeconds > 0 Then the API responds 201 with body containing token, tokenId, partnerId, advocateId (nullable), channel, and expiresAt (UTC) And expiresAt - now is <= ttlSeconds and > 0 And requests with missing/invalid auth return 401; invalid channel or ttlSeconds return 400 with error code and message
Server Validates Token and Maps Attribution on Form Submission
Given a public GiveCrew form includes hidden input source_token with a valid token When a user submits the form Then the created record persists attribution.partnerId, attribution.advocateId (nullable), attribution.tokenId, and attribution.channel = "form" And the API response includes recordId and attribution summary And an audit log entry is created with outcome = "attributed" and tokenId
Share Link Token Attribution on Mobile Web
Given a share link URL contains src_token query parameter with a valid token When a visitor completes a signup within the same browser session Then the resulting record is attributed with partnerId and advocateId from the token and attribution.channel = "link" And if the token is missing, invalid, or expired, the record is created without attribution and the audit log outcome = "unattributed" is recorded
API Submission with Idempotency Key Avoids Duplicate Attribution
Given a partner submits an API request with header Idempotency-Key: K and includes a valid source token When the same request with identical payload is retried with the same key within 24 hours Then only one record is created and subsequent responses return 200/201 with the original recordId and idempotency metadata And only one attribution entry is stored and only one webhook delivery occurs for the set of retries
Webhook Notification on Attributed Record Creation
Given a partner has a webhook endpoint configured When a record is created with valid attribution from a source token Then the system sends a POST webhook within 60 seconds containing recordId, recordType, partnerId, advocateId (nullable), tokenId, timestamp, and a signature header And deliveries are retried with exponential backoff for at least 24 hours until a 2xx is received And idempotent retries of the originating request do not produce additional webhook deliveries
Invalid, Tampered, or Expired Token Handling and Audit
Given a submission includes a token that fails signature verification or is expired When the submission is received via API Then the API responds 422 with error code in [invalid_token, token_expired] and no record is created When the submission is received via public form or share link Then the record is created without attribution, no attribution webhook is sent, and an audit entry with outcome = "token_rejected" is recorded
Channel Binding Enforcement for Tokens
Given a token is generated for channel = "api" When it is presented in a form or link submission Then attribution is rejected per invalid token policy and an audit outcome = "channel_mismatch" is recorded Given a token is generated for channel in ["form", "link"] When it is used in an API submission Then the API responds 422 with error code = "channel_mismatch" and no record is created

Dispute Inbox

Lightweight workflow for partners to flag conflicts, comment, and propose resolutions with suggested merges/splits. MergeGuard tracks SLAs and auto-pauses risky syncs until resolved, keeping coalitions coordinated without side-channel threads.

Requirements

Conflict Flagging & Context Capture
"As a coalition organizer, I want to quickly flag a data conflict with relevant context so that the team can resolve it without side-channel confusion."
Description

Enable any partner user to flag a conflict from contacts, donations, signups, shift assignments, or sync logs with a single tap. Provide a structured form to classify the conflict (duplicate, conflicting field values, overlapping shifts, disputed donation, etc.), auto-capture the current and proposed values, source systems, and timestamps, and allow attachments (screenshots, emails). Create a canonical Dispute record with a unique ID linking back to affected entities and the original sync event. Optimize for mobile with offline-safe drafts and autosave. This establishes a lightweight, standardized intake that replaces ad-hoc side channels and ensures every dispute starts with complete, actionable context.

Acceptance Criteria
Single-Tap Flagging From Entity Views
Given a partner user is viewing a contact, donation, signup, shift assignment, or sync log on mobile When the user taps the "Flag Dispute" button on the detail view Then the Dispute form opens in one tap and pre-associates the initiating entity and context And the form opens within 1.0 second at p95 on supported devices And navigating back returns to the original entity view without data loss
Structured Classification Form With Validation
Given the Dispute form is open When the user selects a classification Then the options include: Duplicate Record, Conflicting Field Values, Overlapping Shifts, Disputed Donation, Other And a classification is required to submit And conditional fields render per classification (e.g., field selector for Conflicting Field Values; shift slot selector for Overlapping Shifts) And the form blocks submit until all required fields validate and displays inline errors adjacent to invalid fields
Auto-Captured Context (Values, Sources, Timestamps)
Given the Dispute is initiated from an entity or a sync log Then the form auto-populates read-only captured fields for Current Value(s), Proposed Value(s) when available, Source System(s), and Timestamps from the initiating context And if Proposed Value(s) are not present, the user can add Proposed Value(s) manually via field-level selectors And all captured and user-entered values are persisted on submit and are visible on the resulting Dispute record
Attachments: Screenshots and Emails
Given the Dispute form is open When the user adds attachments Then the system accepts images (JPG, PNG), PDFs, and EML files And allows at least 5 attachments per Dispute And shows a preview (thumbnail or filename), size, and a remove action for each file And if offline, attachments queue locally and auto-upload on reconnect without blocking Dispute submission
Canonical Dispute Record Linking and ID
Given the user submits a valid Dispute form Then a Dispute record is created with a unique, immutable ID in the format DIS-YYYYMMDD-######## And the record links to all affected entity IDs and, when applicable, the originating sync event ID And the record stores classification, captured values, sources, timestamps, attachments, submitter user ID, and created_at And the Dispute is visible in the Dispute Inbox and from linked entities within 2 seconds of submit
Offline Drafts and Autosave on Mobile
Given the device is offline or loses connectivity When the user begins entering data in the Dispute form Then a local draft is created on first input and autosaves on every field change and at least every 10 seconds And drafts persist through app restarts and are restored when the form is reopened And submitting offline queues the Dispute and auto-submits within 10 seconds of reconnect And the UI clearly indicates draft, queued, and submitted statuses
Partner User Access and Audit Trail
Given an authenticated partner user Then the "Flag Dispute" entry point is visible on supported entities for all partner roles And unauthenticated or non-partner users cannot access the entry point or Dispute form And every create and submit action logs user ID, timestamp, device identifier, and IP address (when online) to an immutable audit trail
Threaded Discussion & Mentions
"As a partner contributor, I want to discuss disputes and tag the right people so that we can reach a resolution quickly."
Description

Provide per-dispute, chronological comment threads with @mentions, emoji reactions, and system event posts (e.g., "owner assigned", "SLA escalated"). Send notifications via in-app, email, and SMS according to user preferences and quiet hours. Support read receipts, attachments in-line, and linkbacks to related records. Include minimal moderation (edit/delete window, redaction for PII). This keeps all communication and evidence centralized, accelerating consensus and eliminating fragmented conversations.

Acceptance Criteria
Chronological Threading & System Events Visible
Given a dispute thread with existing posts, When a new comment is submitted, Then it appears at the end of the thread in chronological order with an ISO-8601 timestamp in the organization’s timezone. Given a system action occurs (e.g., owner assigned, SLA escalated), When the event is emitted, Then a non-editable system event post is appended with standardized label, actor, timestamp, and before/after values. Given a thread contains more than 50 items, When the user scrolls, Then items are paginated in batches of 50 without breaking chronological order. Given a post is updated due to moderation or retention, When rendered, Then an audit badge (e.g., “edited”) is shown with the modification timestamp.
@Mentions with Multi-channel Notifications & Quiet Hours
Given a user types @ and selects a teammate or partner contact, When the comment is posted, Then the mentioned user is tagged and a notification event is queued for them. Given the recipient’s channel preferences, When sending notifications, Then deliver in-app immediately and deliver email/SMS only if enabled in preferences. Given the mention occurs during the recipient’s quiet hours, When processing notifications, Then suppress email/SMS until quiet hours end while posting an in-app notification silently. Given multiple mentions of the same user within 2 minutes on the same thread, When sending notifications, Then send a single consolidated notification summarizing the mentions. Given a notification delivery fails on a preferred channel, When retrying, Then attempt up to 3 retries over 5 minutes and then fall back to the next enabled channel, recording final delivery status.
Emoji Reactions on Comments & Events
Given a comment or system event post, When a user adds a supported emoji reaction, Then the reaction count increments and the user is recorded as a reactor. Given a user taps a reaction they previously added, When toggled off, Then the reaction count decrements and the reaction disappears when count reaches zero. Given a post has 10 or more distinct reactions, When rendered, Then display the top 5 by count with a “+X” overflow indicator and show the full list on tap/click. Given permission rules, When reacting to system event posts, Then reactions are allowed but the event content remains non-editable.
Read Receipts per Message
Given a comment is posted, When a user views the thread in-app and the comment enters the viewport, Then a per-user read receipt is recorded with first-seen timestamp. Given an email or SMS notification contains a secure deep link to the comment, When the recipient opens the link, Then the comment is marked as read for that user. Given a user has disabled read receipts in privacy settings, When they view the comment, Then their read status is hidden while aggregate read counts still reflect their read. Given a comment remains unread by a mentioned user for 24 hours and reminders are enabled, When quiet hours permit, Then send a single unread reminder via the user’s next eligible channel.
Inline Attachments with Safe Handling
Given a user attaches files to a comment, When uploading, Then allow up to 5 files with a maximum of 25 MB each and show progress per file. Given an attachment upload completes, When rendering the comment, Then show inline previews for images and PDFs and file chips with type/size for other formats. Given antivirus scanning is pending, When the comment renders, Then show a “Scanning…” state and block download until the scan passes; if the scan fails, block access and show a security warning. Given a large image is attached, When previewed, Then serve an optimized preview under 1 MB while preserving the original file for download. Given an attachment is removed within the edit window, When the comment is saved, Then delete the object from storage and remove all references.
Linkbacks to Related Records and Deep Links
Given a comment includes a linkback to a related record (contact, donation, shift), When rendered, Then display a typed pill with icon and open the record in a new view on click/tap. Given a deep link in email/SMS is opened, When the app authenticates the user, Then land directly on the target dispute thread and anchor to the specific comment. Given a user lacks permission to a linked record, When they attempt to open it, Then show an access denied message and hide sensitive metadata. Given a related record has been merged or split, When resolving linkbacks, Then route to the canonical record and display a “merged from” or “split from” indicator.
Moderation: Edit/Delete Window & PII Redaction
Given a user posts a comment, When within 10 minutes of posting, Then the author can edit or delete the comment; after 10 minutes, editing is locked and delete requires moderator role. Given a comment is edited or deleted, When displayed, Then show an “edited” or “deleted by moderator” label with timestamp and retain an admin-only audit trail of the original. Given PII patterns (e.g., email, phone, SSN) are detected on submit, When prompted and the user accepts, Then automatically redact PII (masking characters) and do not store the unredacted value. Given manual redaction is applied to a historical comment, When exported or rendered, Then show “[redacted]” in place of masked content and exclude the original from exports and API responses.
Resolution Proposals: Merge/Split With Preview
"As a data steward, I want to propose merges or splits with a clear preview so that I can resolve conflicts confidently without breaking downstream syncs."
Description

Allow users with appropriate permissions to propose concrete resolutions: merge records (select survivor and field-level precedence), split a composite record (household/org/contact), keep-both with designated scopes, or edit specific fields. Show a live preview of the resulting records, field-level diffs, and simulated downstream effects (impacted shifts, communications, receipts). Validate proposals against coalition rules and highlight risky changes. Enable one-click apply after consensus, logging rationale and approvers.

Acceptance Criteria
Merge Proposal: Survivor & Field Precedence Preview
Given a user with Resolve Disputes permission in coalition X and two candidate records A and B are selected in the Dispute Inbox And the user selects survivor record A And the user configures field-level precedence for Name, Email, Phone, and Address When the user opens the Preview pane Then the preview renders the resulting merged record with field-level diffs highlighted within 1 second And a simulated impact summary displays counts for impacted shifts, scheduled communications, and receipts And the proposal can be saved as Draft with a unique ID and timestamp And the Apply action remains disabled until required approvals are met
Merge Proposal: Coalition Rule Validation & Risk Highlighting
Given coalition X has validation rules including: - Block: cannot merge records with different program_scope values - Warn: high risk if more than 3 fields will be overwritten or donor_status changes And the user has configured a merge proposal that triggers both a block and a warning When the user clicks Validate Then blocking violations are displayed with error labels and specific rule names; Save and Apply are disabled And warnings are displayed with warning labels and specific rule names; Save is allowed only after the user acknowledges all warnings And risky fields are annotated inline in the preview And MergeGuard marks related syncs as Paused until the proposal is applied or dismissed
Split Proposal: Composite Record into Separate Records
Given a composite Household record H with mixed contact/org fields, 5 upcoming volunteer shifts, and 2 pending receipts And the user chooses Split and maps fields to new records Contact C' and Organization O' When the user opens the Preview pane Then two resulting records are displayed with their field assignments and relationship links And simulated impacts show reassignment of 5 shifts and 2 receipts to the appropriate new records And validation confirms all required fields for Contact and Organization are present and unique keys are not duplicated And the proposal can be saved and later applied
Keep-Both Resolution with Designated Scopes
Given two duplicate-looking records R1 and R2 belong to different partner scopes (Partner A, Partner B) When the user selects Keep-Both and assigns scopes Partner A to R1 and Partner B to R2 Then the preview shows both records with visible scope tags and dedupe keys And validation confirms scopes are non-overlapping and cross-scope sync rules prevent future auto-merge And the simulated effects show zero reassigned shifts/receipts and updated scope visibility And after apply, subsequent dedupe scans do not suggest re-merging R1 and R2 across these scopes
Targeted Field Edit with Live Diff & Effect Simulation
Given a record R has an incorrect Email value and an upcoming receipt and scheduled communications reference that field When the user proposes to edit Email from old@example.org to new@example.org and enters a required rationale Then the preview shows a field-level diff for Email And simulated effects show counts of items to be updated (e.g., 1 receipt, 2 communications) and allow drill-down And validation enforces email format and coalition domain allowlist rules And the proposal cannot be saved without a rationale of at least 10 characters
One-Click Apply After Consensus with Audit & Notifications
Given coalition X requires 2 approvers and proposal P has 2 unique approvals recorded with no blocking violations When an authorized user clicks Apply Then the system applies the resolution and updates records within 5 seconds And an audit log entry captures proposal ID, rationale, approvers, applier, timestamp, and before/after diffs And notifications are sent to watchers and involved partners within 60 seconds And MergeGuard unpauses any previously paused syncs related to the affected records And actual impacted shifts, communications, and receipts match the prior simulation counts
MergeGuard SLA & Auto-Pause Controls
"As a coalition admin, I want MergeGuard to enforce SLAs and pause risky syncs so that data quality is protected until disputes are resolved."
Description

Introduce configurable SLAs per dispute type, partner, and severity, with timers, reminders, and escalation paths (role-based and channel-based). While a dispute is open and classified as risky, automatically pause related sync operations (e.g., dedupe merges, assignment auto-fill, outbound receipts) and queue changes. Resume automatically on resolution or escalate if the SLA is breached. Provide visibility badges on affected records and a "Why paused" explainer. All actions are auditable to satisfy coalition governance.

Acceptance Criteria
Configure SLA Matrix by Dispute Type, Partner, and Severity
Given an org admin with MergeGuard permissions When they create or edit SLA rules keyed by dispute type, partner, and severity with defined response and resolution time targets Then the system validates fields (non-negative durations, unique key per combination) and persists the rules And shows the computed effective SLA for any combination including fallback defaults when a direct match is missing And when a new dispute is created or reclassified, the effective SLA is automatically assigned and visible on the dispute
SLA Timers, Reminders, and Escalation Paths
Given a dispute with an assigned effective SLA and an owner When the dispute is opened Then a countdown timer starts for response and resolution targets and updates in real time And reminder notifications are sent at configured intervals to the current assignee via the configured channels (e.g., in-app, email, SMS) And if the dispute remains unresolved at each escalation threshold, escalate to the next role(s) and channel(s) as configured, without duplicating prior notifications And reminders and escalations stop immediately once the dispute status changes to Resolved or Not Risky
Auto-Pause Risky Syncs While Dispute Open
Given a dispute marked Risky = true with linked records in scope When a sync operation targets any linked record (dedupe merge, assignment auto-fill, outbound receipts, or other configured risky operations) Then the operation is not executed and is added to the queue with operation type, target IDs, and enqueue timestamp And unaffected operations on records not in scope continue as normal And the dispute header and affected records display Paused status
Queue and Resume Changes on Resolution or Reclassification
Given queued operations accumulated during a Risky pause When the dispute is set to Resolved or reclassified to Not Risky Then the system automatically resumes queued operations FIFO within the next processing cycle And each operation re-validates against current record state before execution; on validation failure the operation is skipped and flagged with error details And a resumptions summary is recorded with counts of succeeded, skipped, and failed operations
SLA Breach Handling and Escalation Continuity
Given a dispute with an active SLA and Risky = true When the resolution timer reaches 0 without the dispute being Resolved Then the system records an SLA Breached event, triggers the configured highest escalation path, and maintains the pause state And a high-visibility badge "SLA Breached" appears on the dispute and affected records And governance subscribers are notified via configured channels with a link to the dispute
Visibility Badges and "Why Paused" Explainer
Given any user viewing an affected record or the dispute detail When the record is paused due to a Risky dispute Then a badge indicates Paused due to Dispute with the current timer status (time remaining or breached) And a Why paused explainer reveals the blocking dispute ID, owner, classification (Risky/Not Risky), effective SLA targets, next reminder/escalation time, and resolution prerequisites And links allow navigation to the dispute thread; the badge and explainer disappear within one refresh cycle after unpause
Auditable MergeGuard Actions
Given auditing is enabled by default for MergeGuard When users or the system perform actions (SLA config changes, dispute creation/reclassification, pause/resume events, reminders, escalations, queue add/execute/skip failures) Then an immutable audit log entry is created with who, what, when, before/after values, and related record IDs And audit entries are queryable by date range, dispute ID, partner, and action type and are exportable in CSV/JSON And access to audit logs is role-restricted and attempts to access without permission are logged and denied
Dispute Assignment, Triage, and Permissions
"As a triage lead, I want to assign owners and control access so that the right people can resolve disputes while sensitive data stays protected."
Description

Offer a triage inbox with filters (type, age, severity, partner, SLA stage) and bulk actions. Support owner assignment, watchers, due dates, and labels. Enforce role-based access controls, including limited partner access, field-level redaction for sensitive PII, and external-sharing toggles. Maintain a complete audit trail of views, edits, and decisions to meet compliance needs. This ensures the right people can act quickly while protecting sensitive information.

Acceptance Criteria
Filter and Sort Disputes in Triage Inbox
Given a user with Triage access and at least 50 disputes across multiple partners, types, severities, and SLA stages When the user applies filters for type=Duplicate, age=7–30 days, severity=High, partner=Partner A, SLA stage=Breached Then only disputes matching all filters are displayed And the total count and pagination reflect the filtered result And the applied filters persist when the user navigates away and back within the session And the user can sort by Age (asc/desc) and Severity with results updating within 2 seconds for up to 1,000 disputes And a clear "No results" state appears when no records match
Bulk Update from Triage (Assign, Labels, Due Dates, Watchers)
Given a user with edit permission selects 10 disputes in the triage inbox When the user performs bulk actions: assign owner=User X, add watchers=[User Y, User Z], apply labels=[Data-Conflict, Urgent], set due date=2025-08-31 Then all selected disputes are updated consistently And items the user lacks permission to modify are skipped with an inline reason per item And a single confirmation step summarizes the changes before apply And within 60 seconds an email/in-app notification is sent to the new owner and added watchers And each updated dispute receives an audit entry detailing the bulk action including initiator, timestamp, fields changed, and before/after values And the operation supports undo for 5 minutes or until any record is further edited
Owner, Watchers, Due Date, and Labels (Single Dispute)
Given a user with edit permission opens a dispute detail When the user assigns an owner, adds/removes watchers, sets a due date, and applies up to 5 labels from the org’s taxonomy Then the owner is displayed in triage list and detail as responsible And watchers cannot be duplicated and may include cross-partner users only if coalition-level permission is granted And due date cannot be in the past and warns if later than the dispute’s SLA required-by date And labels are validated against taxonomy and limited to 5 per dispute And notifications are sent to the owner and watchers immediately, with a reminder queued 24 hours before due date
Role-Based and Partner-Limited Access Enforcement
Given the following roles exist: Org Admin, Coalition Lead, Partner Staff, Volunteer When a Partner Staff user accesses the triage inbox Then they can see only disputes where their partner is owner, reporter, or explicitly shared And PII fields are redacted per policy for users without clearance And attempts to view non-permitted disputes or fields return a 403 and are logged And only Org Admins and Coalition Leads can reassign owners across partners or change external-sharing toggles And access changes take effect within 1 minute of a role or membership update
Field-Level Redaction of Sensitive PII
Given a user without PII clearance opens a dispute containing sensitive fields (donor email, phone, address, card last4, notes marked Sensitive, attachments tagged PII) When viewing the triage list and dispute detail Then sensitive values are masked (e.g., ema***@***.com, (***) ***-****, **** **** **** 1234) and attachments are hidden behind a "Restricted" placeholder And redacted fields remain searchable only by authorized users; unauthorized searches return no PII snippets And authorized users can toggle "Show PII" per session with MFA, expiring after 15 minutes of inactivity And all PII reveals are logged with user, field, timestamp, and reason code
External Sharing Controls for Dispute Threads
Given coalition collaboration is enabled When a Coalition Lead marks a dispute as shareable with Partner B and selects which comments/attachments are external Then Partner B users with access can view only the external-marked items; internal-only content remains hidden And external share links expire after 14 days by default and can be revoked immediately And sharing is blocked if items contain PII and the recipient role lacks PII clearance, prompting the lead to remove or redact And all shares and unshares create audit entries and notify the current owner
Comprehensive Audit Trail of Views, Edits, and Decisions
Given audit logging is enabled When any user views, edits, bulk-updates, changes sharing, or resolves a dispute Then an immutable audit record is created capturing actor, action, timestamp (UTC), IP, fields changed with before/after, and related IDs And an administrator can export audit records for a date range and dispute IDs to CSV/JSON within 60 seconds for up to 100,000 events And audit records are retained for at least 7 years and are tamper-evident via chained hashing And audit search supports filters by actor, action type, dispute ID, partner, and time window
Resolution Apply, Rollback, and Audit Events
"As an operations manager, I want applied resolutions to be auditable and reversible so that we can recover quickly from mistakes and maintain trust."
Description

Apply approved resolutions atomically and idempotently across the CRM and connected integrations. Generate detailed change logs capturing before/after values, approvers, and timestamps, and emit webhook events for partner systems. Create rollback snapshots and a one-click revert path with guardrails when dependencies have changed. Present a post-resolution summary and close the dispute with outcome codes to feed reporting and learning loops.

Acceptance Criteria
Atomic, Idempotent Apply Across CRM and Integrations
Given an approved resolution with a defined change set across multiple CRM records and connected integrations When Apply is executed once Then all targeted CRM records reflect the new values in a single commit, no partial updates are visible, and the dispute status updates to Applied And outbound integration updates are dispatched exactly once per target using an idempotency key And if any step fails, no changes persist and the user sees a single error with a correlation ID Given the same resolution is re-applied with the same idempotency key due to a retry When Apply is executed again Then no duplicate side effects occur in CRM or integrations and the response indicates idempotent replay
Comprehensive Audit Log of Resolution Apply
Given a resolution is successfully applied When the operation completes Then an immutable audit entry is created containing: disputeId, resolutionId, actorId, approverId, timestamp (UTC ISO-8601), outcomeCode, correlationId, and a per-field before/after list for each affected entity And the audit entry is queryable by disputeId and exportable as JSON and CSV And any correction is appended as a new audit record referencing the original; direct edits are blocked And viewing audit entries requires Dispute.Audit.View permission and is denied otherwise (403)
Webhook Events Emission and Reliable Delivery
Given a resolution apply succeeds When events are emitted Then a dispute.resolution.applied webhook is sent with payload including eventId (UUIDv4), occurredAt, disputeId, resolutionId, outcomeCode, idempotencyKey, and a changeSet hash And the request is HMAC-SHA256 signed and includes a signature header; partners acknowledging with 2xx stop retries And retries use exponential backoff for up to 24 hours with at least 10 attempts; 4xx stops after 2 attempts; 5xx continues until exhausted And events are de-duplicated on partner side via eventId and on our side via idempotencyKey And failed deliveries after max retries are recorded in a dead-letter queue visible in Integration Health
Rollback Snapshot and One-Click Revert with Guardrails
Given a resolution has been applied When the apply completes Then a rollback snapshot capturing the pre-apply state of all affected entities is stored with a minimum 30-day retention and linked to the dispute Given an authorized user initiates a rollback When dependency checks detect intervening changes (version mismatches, subsequent merges/splits) Then the system surfaces a diff, requires explicit confirmation with a reason, and blocks auto-rollback if restoring would orphan related data When rollback succeeds Then all entities are restored to the snapshot state atomically, a dispute.resolution.rolled_back webhook is emitted, and an audit entry records actorId, timestamp, and reason And if rollback cannot complete safely, no partial reversions occur and the user is prompted to open a new resolution draft
Post-Resolution Summary and Dispute Closeout
Given a resolution apply has completed When the user views the completion screen Then a summary displays: count of records changed, fields modified, integrations notified with latest delivery status, SLA timers cleared/paused, outcomeCode, and links to the audit log and webhook events And the dispute status transitions to Closed with an outcomeCode in [Merged, Split, Reassigned, Dismissed, Other] And selecting Other requires a non-empty reason of at least 10 characters And the Impact Board metrics update within 5 minutes to include the closed dispute and time-to-resolution
Concurrency Control and Authorization for Apply/Rollback
Given two users attempt to apply the same approved resolution concurrently When both actions are submitted Then only one apply proceeds and the other receives an Already Applied response with no side effects Given a resolution is not Approved When Apply is attempted Then the system blocks the action with Requires Approval and logs the attempt in the audit trail Given a user lacks Dispute.Apply or Dispute.Rollback permissions When they attempt via UI or API Then the actions are hidden in UI and API requests return 403 with a correlation ID And all actions record actorId from the authenticated session or service account with disputes.write scope

Smart Step-Up

Personalizes the one-tap upsell amount and message based on each donor’s history, current gift, and campaign urgency. Suggests the most likely-to-convert add-on (e.g., +$3, +$5, +$10) and A/B tests copy in the background. Donors see the immediate thermometer jump, driving more micro-pledges without fatigue.

Requirements

Personalized Upsell Amount Engine
"As a donor, I want a suggested add-on that fits my giving pattern so that it feels relevant and easy to accept."
Description

Computes a donor-specific add-on amount (e.g., +$3, +$5, +$10) using donor history, current gift size, recency, giving pattern, and campaign urgency to maximize conversion without over-asking. Implements a rules-first model with pluggable scoring (weighted heuristics initially; ML-ready interface) and fallbacks when data is sparse. Provides an API/service that returns the suggested amount, expected uplift, and confidence, with guardrails for minimum/maximum increments, currency/locale handling, and rounding rules. Logs inputs/decisions for auditing, supports real-time and batch precomputation, and degrades gracefully if donor data is unavailable.

Acceptance Criteria
Real-time API returns personalized upsell suggestion within SLA
- Given a donor profile with history, currentGift, recency, givingPattern, and campaignUrgency - When the client requests a suggestion via the upsell API with valid auth and payload - Then the response status is 200 and includes fields: suggestion.amount (number), suggestion.currency (ISO 4217), expectedUplift (0..1), confidence (0..1) - And suggestion.amount represents the increment (not total gift) computed by rules-first model with optional scorer input - And P95 latency <= 200 ms and P99 latency <= 400 ms over the last 24 hours - And identical inputs within a 10-minute window produce the same suggestion amount
Guardrails and currency/locale rounding enforced
- Given computed rawIncrement and configured minIncrement, maxIncrement, and stepByCurrency - When the final suggestion is produced - Then final amount is clamped to [minIncrement, maxIncrement] and rounded to the currency’s minor unit and configured step - And JPY suggestions have no fractional digits; USD/EUR are rounded to 2 decimals - And if the computed amount <= 0, replace with minIncrement - And if campaignMaxGiftIncrement is configured, final amount + currentGift does not exceed that limit
Fallback behavior with sparse or unavailable donor data
- Given donor history is missing, incomplete, or data services time out - When a suggestion is requested - Then the engine returns a default increment from campaign policy or global defaults and sets decisionSource = "FALLBACK" - And confidence <= 0.40 and expectedUplift is populated from baseline metrics - And the HTTP response remains 200 with a degradation flag; no stack traces or internal errors are exposed - And total response P95 latency remains <= 350 ms with at most one retry to the data service
Structured decision logging and audit trail
- Given any upsell decision is produced (real-time or batch) - When logging is enabled - Then a structured log is written with: timestamp(ISO8601 UTC), requestId, hashedDonorId, campaignId, inputSummary, rulesApplied, scorerVersion, guardrailsApplied, suggestion.amount, expectedUplift, confidence, latencyMs, decisionSource - And PII (name, email, phone) is never logged; identifiers are hashed with rotating salt per policy - And logs are queryable within 5 minutes of event time and retained for >= 90 days - And errors record errorCode and redacted stack trace
Batch precomputation and caching for active campaigns
- Given a batch job for N donors and a campaign is triggered - When the job runs - Then suggestions are computed and stored with TTL (default 24h) in cache/DB keyed by donorId+campaignId+version - And throughput >= 50,000 suggestions/hour with success rate >= 99.5% - And partial failures retry up to 3 times with exponential backoff without duplicating stored results - And reruns are idempotent; newer versioned results overwrite older ones only when version increases
Pluggable scoring with rules-first precedence and graceful degradation
- Given a scorer plugin implementing interface v1 is available - When feature flag scorer=ML is enabled for a campaign - Then the engine invokes the scorer to produce a raw score which is transformed to an increment and then passes guardrails - And if the scorer errors or exceeds 100 ms, the engine falls back to heuristic scoring without failing the request and sets decisionSource = "HEURISTIC_FALLBACK" - And with scorer disabled, outputs differ from heuristic by no more than ±1 step on seeded fixtures - And scorer selection is configurable per campaign via settings
Response schema and versioning contract
- Given any API response from the upsell engine - Then it conforms to schema: version(semver), suggestion:{amount:number>0, currency:ISO4217}, expectedUplift:number in [0,1], confidence:number in [0,1], decisionSource:string, guardrails:{min:number, max:number, rounded:boolean}, metadata:{requestId:string} - And unknown additional fields are ignored by clients; breaking changes only under a major version bump - And all numeric fields are finite and non-NaN; currency matches ISO 4217 - And the published OpenAPI/JSON Schema exists and passes contract tests in CI
Context-Aware One-Tap Prompt
"As a supporter completing a donation, I want a one-tap prompt at the right moment so that I can quickly boost my impact."
Description

Surfaces the upsell prompt at the optimal moment in the donation flow (post-amount, pre-finalize, or immediately after authorization where payment provider allows incremental capture), with a single-tap accept and clear decline option. Adapts placement and timing by channel (mobile app, web embed, kiosk) and supports low-bandwidth/offline queuing with eventual sync. Provides accessible UI (WCAG AA), localized copy, and small footprint for fast render. Integrates with the payment processor to add an incremental charge safely, handling authorization windows, idempotency, and error recovery. Offers configurable display criteria (gift thresholds, campaign tags, donor segments).

Acceptance Criteria
Mobile App: Post-Amount Prompt Timing & One-Tap UX
Given a donor enters a valid donation amount in the mobile app and taps Continue When the upsell rules evaluate true for the donor and campaign Then the one-tap upsell prompt appears within 300ms after the amount screen and before the final review step And the prompt shows a single primary Accept action and a clearly labeled Decline option And the suggested add-on is selected from +$3/+5/+10 based on donor history and current gift When the donor taps Accept once Then the add-on is applied to the pending total and the donor proceeds to finalize without additional steps And the campaign thermometer increases immediately by the add-on amount (optimistic update) and persists after successful charge And if the upsell charge fails, the thermometer reverts within 5s and an error message is shown And if the donor taps Decline, no add-on is applied and the prompt is suppressed for the remainder of the checkout session And no more than one upsell prompt is shown per checkout session And all interactions (view, accept, decline) are logged with timestamp, donor (hashed), device, and variant id
Web Embed: Post-Authorization Incremental Capture with Idempotency
Given the payment provider supports incremental capture and the authorization window has ≥ 5 minutes remaining And the donor has authorized the base donation amount on the web embed When the upsell rules evaluate true Then the upsell prompt appears within 500ms after base authorization with Accept and Decline options When the donor taps Accept once Then an incremental capture is created using an idempotency key composed of {auth_id + session_id + upsell_config_id} And duplicate taps or page refreshes do not create duplicate charges And on transient gateway/network errors, the system retries up to 3 times with exponential backoff (max 8s) And if the authorization expires before success, the upsell is not charged and an inline notice explains that the window closed And the donor’s receipt shows separate base and upsell line items and is sent within 30s of success And choosing Decline proceeds to confirmation without additional charges and suppresses further prompts in this session And all gateway responses (codes, ids) and outcomes are recorded in the audit log
Kiosk: Offline Queueing and Eventual Sync
Given a kiosk is offline or experiencing low bandwidth When a donor qualifies for the upsell and the prompt is shown from cached assets Then tapping Accept creates a queued upsell operation with a unique id, donor token, suggested amount, and consent timestamp And the UI confirms Added and continues flow without delaying checkout And the local queue stores up to 500 pending operations or 1 MB, whichever comes first, with oldest entries pruned after 72 hours When connectivity resumes Then queued operations attempt sync within 2 minutes and charge incrementally within the authorization window And successful sync updates donor history and campaign totals, and clears the queue item And failures after 3 retries mark the item Failed with a visible operator alert and no duplicate attempts And all queued operations use idempotency keys to prevent duplicate charges upon retries
Accessibility: WCAG AA Compliance for Prompt
Given a user relies on assistive technologies (screen reader, keyboard-only, switch access) When the upsell prompt opens Then focus moves to the dialog heading and is trapped within the dialog until Accept/Decline/Close And the dialog has valid name, role=dialog, aria-modal=true, and controls have accessible names And the prompt is fully operable via keyboard (Tab/Shift+Tab/Enter/Escape) and by screen reader rotor navigation And color contrast for text is ≥ 4.5:1 and non-text UI elements are ≥ 3:1; touch targets are ≥ 44x44 px And animations respect prefers-reduced-motion and no content flashes more than 3 times per second And the component passes automated accessibility tests (axe-core/Pa11y) with zero critical/serious issues and manual checks for focus order, announcements, and reflow (WCAG 2.2 AA) And closing the prompt returns focus to the previously focused control
Localization: Language, Currency, and RTL Support
Given the donor’s locale and currency are detected or selected When the upsell prompt renders Then all strings use the correct localized copy for that locale, with fallback to org default if missing And currency, dates, and numbers are formatted per locale (e.g., €5,00 for fr-FR) And right-to-left locales render correctly with mirrored layout and proper text direction And the selected copy variant (A/B) is the locale-specific version of the same variant id And 100% of prompt strings are covered by translations for supported locales; missing keys are logged and surfaced to admins And switching locale mid-session re-renders the prompt with the new locale without a full reload
Configurability: Display Rules by Thresholds, Tags, and Segments
Given an admin configures display rules (e.g., min gift ≥ $10, campaign tags include “Urgent”, donor segment not in “Do-Not-Upsell”) When a donor progresses through checkout Then the upsell prompt displays only when all configured criteria evaluate true for that donation And when any criterion evaluates false, the prompt is not shown and the decision reason is recorded (suppressed_by_rule=<rule_id>) And admins can preview rules on a test donation to see a pass/fail explanation for each rule And rule changes take effect for new sessions within 10 minutes and are versioned with rollback capability And channel-level overrides (mobile/web/kiosk) can enable/disable the prompt per channel And all rule evaluations are logged with rule version, inputs, and outcome
Performance: Fast Render and Small Footprint Across Channels
Given a cold-start web embed on a 4G connection (400 ms RTT, 1.6 Mbps) When the amount step loads Then the upsell prompt adds ≤ 20 KB gzipped to initial payload and ≤ 1 additional request, with first render in ≤ 300 ms p50 and ≤ 600 ms p95 And on low-end Android (Android 10, 2 GB RAM), main-thread work attributable to the prompt is ≤ 50 ms p95 And on kiosk with cached assets, the prompt appears in ≤ 150 ms from trigger And memory overhead attributable to the prompt stays ≤ 15 MB p95 during interaction And no long tasks (>200 ms) are introduced by the prompt scripts/styles And performance metrics (TTFB, FCP, INP for prompt interactions) are captured and reported with sample rate ≥ 10%
A/B Copy Optimization with Auto-Winner
"As a campaign manager, I want Smart Step-Up to test message variants automatically so that we find the highest-converting copy without manual effort."
Description

Delivers multiple micro-copy variants for the upsell prompt and automatically allocates traffic to higher performers over time (multi-armed bandit or adaptive A/B). Tracks conversion, average add-on, and decline rates by segment (new vs. returning donors, device, campaign). Enforces minimum sample sizes and confidence thresholds before declaring a winner, and supports scheduled or manual variant rotation. Provides a variant library with versioning, preview, and rollback, plus exportable results for reporting. Integrates with analytics events to attribute incremental revenue and suppresses variants that trigger elevated declines or refunds.

Acceptance Criteria
Adaptive Traffic Allocation (Multi-Armed Bandit)
Rule: Reward per impression (RPI) = conversion_rate × average_add_on. Given 3+ active variants and bandit mode enabled, When each variant reaches ≥50 impressions, Then the system updates traffic weights at least every 15 minutes based on observed RPI, and no variant’s weight drops below 5% until it has ≥200 impressions. Given variant A’s RPI is ≥20% higher than variant B with ≥95% Bayesian credible separation after both have ≥200 impressions, When the next allocation cycle runs, Then A receives ≥2x B’s traffic share. Given a variant is paused or archived, When allocation runs, Then its traffic weight becomes 0% within 5 minutes.
Winner Declaration with Minimum Sample and Confidence
Given two or more active variants, When each has ≥400 impressions and the top variant’s RPI is ≥10% higher than the next-best with ≥95% confidence, Then the system declares a winner and sets its traffic to ≥80% within 10 minutes. Given the above thresholds are not met, Then no winner is declared and adaptive allocation continues. Given a winner is declared, When subsequent data reduces confidence below 90%, Then the system reopens the test, restores adaptive allocation within 15 minutes, and records an audit event.
Segment-Level Metrics and Reporting
Given active variants, When viewing the results dashboard, Then metrics are displayed by segment: donor_type (new/returning), device_type (iOS/Android/Web), and campaign_id, including impressions, conversions, conversion_rate, average_add_on, RPI, decline_rate, refund_rate for each segment-variant. Given "Export Results" is clicked, Then a CSV is generated within 10 seconds containing one row per variant × segment × day with all metrics and timestamps, and totals match on-screen aggregates within 0.5%. Given filters (date range, campaign) are applied, Then dashboard visuals and exports reflect the filters identically.
Variant Library with Versioning, Preview, and Rollback
Given a user creates or edits a variant, Then a new immutable version record is stored with version_id, author, timestamp, copy payload, and targeting rules. Given "Preview" is requested for a version, Then the exact prompt renders with token substitution from preview data and a shareable preview URL is generated. Given "Rollback to Version X" is executed, Then Version X becomes the active content within 2 minutes without altering its version_id and an audit log entry is created.
Scheduled and Manual Variant Rotation
Given a rotation schedule is configured (start/end per variant), When the scheduled time occurs, Then the system activates/deactivates variants accordingly within 5 minutes and logs the execution. Given a user triggers "Rotate Now", Then the next scheduled variant set becomes active immediately and bandit allocation applies only to the active set. Rule: Scheduled activation cannot re-activate suppressed variants; safety suppressions take precedence.
Safety Suppression on Elevated Declines/Refunds
Rule: If a variant’s decline_rate over its last ≥200 impressions is ≥50% higher than control and ≥3 percentage points absolute higher, Then auto-suppress (traffic = 0%) within 10 minutes and flag the variant. Rule: If a variant’s refund_rate over cohorts with ≥20 accepted upsells in the last 7 days is ≥2× the median of other active variants and ≥1% absolute, Then auto-suppress within 10 minutes and flag the variant. Given a variant is auto-suppressed, Then an alert is sent to admins via in-app notification and email and the variant is excluded from future allocations until manually reinstated.
Analytics Events and Incremental Revenue Attribution
Given a donor views, accepts, declines, or refunds an upsell, Then an analytics event (view, accept, decline, refund) is emitted within 2 seconds with properties: donor_anonymous_id, donor_type, device_type, campaign_id, variant_id, variant_version_id, allocation_weight, amount_suggested, amount_added, timestamp, and RPI_contribution. Rule: Incremental revenue attributed = Σ(amount_added) − Σ(refunds) for the test window; dashboard and exports reconcile to the transaction ledger within 1%. Given the external analytics sink is temporarily unavailable, Then events are queued and retried with exponential backoff for up to 24 hours with a drop rate <0.1% during a 1-hour outage.
Real-Time Thermometer Lift & Impact Board Sync
"As a donor, I want to see the progress thermometer jump when I add a few dollars so that I feel my contribution matters immediately."
Description

Immediately updates campaign totals and animates the progress thermometer when an upsell is accepted, reinforcing donor impact. Ensures atomic updates to totals to avoid double-counting under concurrency, and syncs the new total to the live Impact Board and printable progress posters. Supports optimistic UI with server reconciliation, handles delayed or failed settlement gracefully, and flags adjustments for audit. Exposes webhooks for external displays and provides caching to minimize load while keeping perceived real-time responsiveness under 500 ms.

Acceptance Criteria
Immediate Thermometer Animation on Upsell Acceptance
Given a donor accepts a one-tap upsell on a campaign When the client registers the acceptance and submits to server Then the progress thermometer animates from previous total to (previous total + upsell amount) within 500 ms and displays the +amount delta Given server confirmation returns a canonical total that differs from the optimistic total When the response is received Then the UI reconciles to the canonical total within 1 second with a smooth adjustment and without duplicating the success animation Given the upsell is ultimately declined or reversed by the processor When the reversal is received Then the thermometer reverts only the upsell delta, shows a non-blocking adjustment notice, and no error blocks navigation
Atomic Total Update Under Concurrent Upsells
Given multiple donors accept upsells on the same campaign within a 100 ms window When the server processes the requests Then the campaign total increases exactly by the sum of distinct accepted upsell amounts with no double counting, verified via unique idempotency keys per upsell Given a client or gateway retries the same upsell request When duplicates arrive Then only one commit is applied per idempotency key and all duplicates return the same committed total Given concurrent commits are in flight When any reader fetches the campaign total Then readers observe only pre-commit or post-commit totals (no partial sums), and commit p95 latency is ≤ 200 ms under expected load
Impact Board and Printable Poster Sync
Given an upsell is successfully committed When the campaign total updates Then the live Impact Board reflects the new total within 1 second and visually indicates the increment Given a printable progress poster asset is cached via CDN When the total updates Then a new versioned asset (e.g., ETag change) is available within 5 seconds and stale assets are invalidated Given an offline Impact Board client reconnects When connectivity is restored Then it receives the latest total within 2 seconds without missing increments
Optimistic UI with Server Reconciliation
Given the client renders an optimistic total immediately after upsell submission When the server responds with success and canonical totals Then the UI updates receipt and total to match the server within 1 second and records the server timestamp Given the server responds with failure or a later settlement reversal occurs When the failure or reversal is received Then the UI reverts only the optimistic upsell delta, shows a non-blocking message, and logs an audit entry with a reason code Given no server response within 3 seconds of submission When a timeout occurs Then the UI shows a non-blocking "processing" state and auto-updates upon eventual server response without user refresh
Settlement Delay/Failure Handling and Audit Trail
Given a delayed capture or failure is reported by the payment processor within 24 hours of the upsell When the system ingests the status change Then it adjusts the campaign total accordingly within 2 seconds, updates the thermometer and Impact Board, and appends an immutable audit entry (upsell ID, prev total, new total, reason, timestamps, actor) Given an admin performs a manual adjustment to the campaign total When the adjustment is saved Then the change is flagged as an adjustment, appears in the audit feed, and does not trigger donor-facing upsell animation Given partial captures or currency rounding differences occur When reconciling amounts Then documented rounding rules are applied consistently and discrepancies are recorded in the audit entry
External Display Webhooks Delivery and Security
Given a campaign total changes due to an accepted or reversed upsell When webhook subscriptions exist Then at-least-once delivery occurs within 2 seconds including payload with campaign ID, new total, delta, event type, and an HMAC-SHA256 signature and idempotency key Given a subscriber endpoint returns 5xx or times out When retries are scheduled Then exponential backoff retries continue for up to 24 hours and duplicate deliveries are safe due to idempotency keys Given a subscriber rotates its signing secret When overlapping keys are configured Then signatures validate during the rotation window without delivery interruption
Caching and Perceived Real-Time Responsiveness
Given CDN/API caching is enabled for total reads When a total changes Then caches are invalidated or bypassed so that p95 perceived UI update is ≤ 500 ms from donor tap to visible thermometer change Given high read load (≥ 1000 RPS p95) When clients fetch totals Then responses are served from cache with freshness ≤ 1 second and include correct ETag/Last-Modified headers Given the client is offline or on a high-latency network When the upsell is submitted Then the last-known total is displayed with an optimistic delta and the UI reconciles to the canonical total within 2 seconds of reconnection
Donor Fatigue Guardrails & Frequency Capping
"As a donor, I want the app to avoid asking me too often so that I don’t feel pressured or fatigued."
Description

Implements per-donor rules to prevent over-asking, including exposure caps (e.g., max N prompts per 30 days), cooldowns after declines, and suppression for donors with recent negative signals (refunds, complaints). Applies campaign-level saturation limits and respects user-level opt-outs and do-not-upsell flags. Provides real-time eligibility checks in the prompt service and transparent reasoning in logs for why a prompt was shown or suppressed. Includes tunable defaults and admin overrides, ensuring legal and ethical fundraising practices while preserving long-term donor goodwill.

Acceptance Criteria
Per-Donor Exposure Cap (30-Day Rolling Window)
Given donor D has 3 recorded upsell prompt exposures in the last 30 days and the global cap is 3 When D triggers an upsell-eligible event Then the decision service must return eligible=false with reason_code="cap_exceeded" and include cap=3, count=3, window_days=30, window_start, and window_end And no upsell prompt is rendered to D And a decision log containing decision_id, donor_id, campaign_id, cap, count, window_days, and reason_code is persisted within 5 seconds and retrievable via the admin logs API Given donor D has 2 recorded upsell prompt exposures in the last 30 days and the global cap is 3 When D triggers an upsell-eligible event Then the decision service must return eligible=true and include pre_decision count=2 and cap=3 And the exposure count increments to 3 only after the prompt is actually rendered And exposures are counted across all upsell variants (A/B) and channels
Cooldown After Decline
Given donor D declines an upsell prompt at time t via an explicit "No thanks" action and the default cooldown is 7 days When any upsell trigger occurs for D before t + 7 days Then the decision service must return eligible=false with reason_code="cooldown_active" and include cooldown_days=7 and cooldown_expires_at And no upsell prompt is rendered to D Given the current time is at or after cooldown_expires_at When D triggers an upsell-eligible event Then the decision service evaluates other guardrails and does not apply the decline cooldown And a decision log records the end of cooldown with eligible status and applied rule set
Negative Signal Suppression (Refunds and Complaints)
Given donor D has a refund recorded within the past 14 days When an upsell eligibility check is performed Then the decision service must return eligible=false with reason_code="negative_signal_refund" and include suppression_days=14 and suppression_expires_at And no upsell prompt is rendered Given donor D has a complaint flag recorded within the past 60 days When an upsell eligibility check is performed Then the decision service must return eligible=false with reason_code="negative_signal_complaint" and include suppression_days=60 and suppression_expires_at Given both a refund (14-day) and a complaint (60-day) exist When an upsell eligibility check is performed Then the decision service selects the later expiration as the effective suppression window and returns that in suppression_expires_at And all decisions are logged with the triggering signal type(s)
Campaign-Level Saturation Cap (Daily)
Given campaign C has daily_upsell_cap=10000 and exposures_today=10000 When any donor triggers an upsell-eligible event for campaign C Then the decision service must return eligible=false with reason_code="campaign_cap_reached" and include exposures_today and daily_upsell_cap And no upsell prompt is rendered for campaign C until the daily window resets And the saturation counter consistency across nodes is maintained with p99 staleness ≤ 1 second Given the daily window resets at 00:00 UTC and exposures_today resets to 0 When the next eligibility check occurs after reset Then decisions are evaluated without the previous day’s saturation affecting eligibility
Respect Opt-Outs and Do-Not-Upsell Flags
Given donor D has profile.do_not_upsell=true When an upsell eligibility check is performed Then the decision service must return eligible=false with reason_code="opt_out" and include source="profile" And no A/B assignment, prompt rendering, or exposure counting occurs Given the request contains upsell_opt_out=true (API or SDK signal) When an upsell eligibility check is performed Then the decision service must return eligible=false with reason_code="opt_out" and include source="request" Given donor D unsubscribed via channel preferences mapped to do-not-upsell When an upsell eligibility check is performed Then the decision service must return eligible=false with reason_code="opt_out" and include source="channel" And all opt-out denials are logged with source and timestamp
Real-Time Eligibility Decision and Explainable Logs
Given live traffic at 1000 RPS with a typical donor/campaign mix When the eligibility API is exercised for 10 minutes Then the p95 latency must be ≤ 100 ms and p99 latency ≤ 200 ms measured at the service boundary Given any eligibility decision (eligible or ineligible) When the decision payload is returned Then it must include: eligible (boolean), reason_code (string), reason_chain (ordered list of applied rules with parameters), and effective_values (cap, window, cooldown, overrides) And a corresponding decision log must be queryable within 5 seconds and contain decision_id correlating the response, inputs (redacted of PII as configured), and applied precedence And logs must include variant_id when applicable without running A/B assignment for ineligible donors
Admin Defaults, Overrides, and Rule Precedence
Given global defaults (cap=3 per 30 days, cooldown=7 days, refund_suppression=14 days) are configured and campaign C sets overrides (cap=2 per 30 days) and donor D has a per-donor override (cap=1 per 30 days) When evaluating eligibility for donor D in campaign C Then the effective values must follow precedence: donor override > campaign override > global default, yielding cap=1 And the decision payload and logs must include the applied precedence and sources for each effective value Given an admin updates a campaign override via the console within allowed ranges (cap between 0 and 10) When the change is saved Then the new values become effective for new decisions within 5 minutes and an immutable audit record is created with actor_id, timestamp, old_value, new_value, scope, and reason And attempts to set values outside safe bounds are rejected with validation errors and no change is applied
Admin Controls & Campaign Configuration
"As an organizer, I want controls to set amount ranges, urgency weighting, and copy variants so that Smart Step-Up aligns with my campaign goals."
Description

Offers a configuration panel where organizers set amount bands, minimum gift thresholds to show the upsell, urgency weighting, campaign eligibility, and copy variants. Includes previews for prompt UI across devices, scheduling (start/stop dates), per-campaign toggles, and safe-edit workflows with draft/publish states. Provides role-based access control, change history, and audit logs. Surfaces KPI snapshots (conversion rate, incremental revenue, average add-on, declines) and recommended tweaks based on observed performance. Ensures configurations propagate instantly with feature-flag support for staged rollouts.

Acceptance Criteria
Amount Bands and Minimum Gift Threshold
Given I set amount bands to [+3, +5, +10] and a minimum gift threshold of $10 in a Draft, When I click Save Draft, Then the draft saves successfully and shows a confirmation without errors. Given a Draft contains duplicate, non-numeric, or non-positive band values, When I click Save Draft, Then the save is blocked and inline error messages identify each invalid field. Given a Published config with threshold $10 and bands [+3, +5, +10], When a donor completes a base gift of $9.99, Then no upsell prompt is shown. Given the same Published config, When a donor completes a base gift of $10.00, Then the upsell prompt displays add-on options +$3, +$5, +$10 in the configured order.
Urgency Weighting Influences Suggested Add-On
Given two otherwise identical campaigns where Campaign A has High urgency and Campaign B has Low urgency and urgency weighting is set to 100% and Published, When the same donor profile gives the same base amount to each, Then the suggested add-on amount for Campaign A is greater than or equal to that for Campaign B. Given urgency weighting is set to 0% and Published, When comparing suggested add-ons for identical donors across campaigns with different urgency, Then the suggested add-on amounts are identical for those donors. Given an admin changes urgency weighting from 0% to 100% and publishes, When I open Change History, Then the new weighting value and actor are recorded in the publish entry.
Campaign Eligibility and Scheduling Controls
Given Smart Step-Up is toggled Off for Campaign A and Published, When donors give to Campaign A, Then the upsell prompt does not render. Given Smart Step-Up is toggled On for Campaign A with a schedule of Aug 10, 2025 08:00 to Aug 20, 2025 23:59 in America/New_York and Published, When the current time is within that window in that time zone, Then the upsell prompt renders for eligible donors; When outside the window, Then it does not render. Given I enable Smart Step-Up for Campaign A and click Publish, When donors in Campaign A complete a gift, Then the upsell eligibility and schedule take effect within 3 seconds at the 95th percentile. Given rollout is set to 25% for the Published configuration, When eligible donors trigger the upsell, Then 23%–27% are bucketed into the new configuration and the remainder see the previous configuration, and cohort assignment remains stable for each donor for 30 days.
Safe-Edit Draft/Publish and Cross-Device Preview
Given a Draft exists, When I change copy variants, amount bands, or thresholds and do not publish, Then live prompts remain unchanged for donors. Given a Draft exists, When I open Preview and select Mobile, Then the prompt renders with the Draft values and no layout clipping; When I select Tablet or Desktop, Then the preview renders appropriately for each size. Given I click Publish Draft and provide a required change summary, When publish succeeds, Then the version number increments and live prompts use the new configuration. Given a previous version exists, When I select Revert to Version N and Publish, Then Version N+1 is created matching Version N and live prompts switch to it.
Role-Based Access Control for Configuration and Publishing
Given a user has Config:Edit but not Config:Publish permission, When they modify a Draft and attempt to Publish, Then the Publish control is disabled in the UI and any API publish attempt returns 403 Forbidden and is audit-logged. Given a user has Config:View only, When they open the configuration panel, Then all fields are read-only and Save/Publish actions are not visible. Given a user has Config:Edit and Config:Publish, When they open the configuration panel, Then they can create, edit, preview, and publish configurations successfully.
Change History and Audit Logging
Given any Publish action completes, When I open Change History, Then an entry shows timestamp, actor, campaign, version, rollout settings, and a field-level diff with previous and new values. Given I filter Change History by date range or actor, When I apply the filter, Then the list updates to show only matching entries. Given an unauthorized publish attempt occurs, When it is blocked, Then an audit log entry records timestamp, actor, campaign, and reason "Forbidden".
KPI Snapshots and Performance Recommendations
Given a campaign has at least 200 upsell prompts shown in the selected date range, When I open the KPI panel, Then it displays conversion rate (% accepts), incremental revenue ($), average add-on ($), and decline rate (%) for that range. Given new upsell events are recorded, When I refresh the KPI panel, Then metrics reflect the new data within 15 minutes. Given conversion rate decreases by 20% or more week-over-week for a campaign with sufficient sample size, When I open Recommendations, Then at least one recommendation is shown referencing copy or amount bands with a rationale based on observed metrics; Otherwise a message indicates insufficient data for recommendations.
Compliance, Consent, and Receipt Itemization
"As a treasurer, I want upsell add-ons to respect consent and be itemized on receipts so that we meet donor expectations and regulatory requirements."
Description

Ensures the upsell clearly conveys that an additional amount will be charged, with explicit accept/decline, and records consent with timestamp, device, and variant metadata. Itemizes the add-on in receipts and acknowledgments, including tax-deductibility notes and campaign designation, and updates donor records accordingly. Handles edge cases such as recurring gifts, employer matches, and restricted funds, and adheres to jurisdictional requirements for charitable solicitations. Provides refund/reversal pathways for the add-on only and maintains an auditable trail for finance and compliance reviews.

Acceptance Criteria
One-Tap Upsell Consent Capture and Clear Disclosure
Given a donor has entered a primary gift and the Smart Step-Up prompt is shown Then the prompt displays the exact add-on amount and clearly states that the additional amount will be charged immediately if accepted And the prompt presents two distinct actions: “Accept Add-On” and “No Thanks,” with neither preselected And no add-on is charged unless the donor taps “Accept Add-On” When the donor taps “Accept Add-On” Then the system records a consent event with fields: donorId, campaignId, primaryGiftAmount, addOnSuggested, addOnAccepted, currency, timestamp (ISO 8601), device (OS and browser user-agent), IP address, experimentId, variantKey, sessionId, paymentIntentId/transactionId And the consent event is persisted prior to payment capture and linked to the payment transaction And the add-on amount is authorized and captured successfully When the donor taps “No Thanks” Then no add-on is charged and a decline event with the same metadata is recorded And the consent/decline event is retrievable via a Compliance Export filtered by date range, campaign, and experimentId
Add-On Receipt Itemization and Tax-Deductibility Notes
Given a payment completes with an accepted add-on Then the email receipt, SMS receipt (if enabled), and in-app receipt each display two separate line items: Primary Gift and Add-On And each line item shows amount, currency, and campaign/fund designation And each line item shows tax-deductible amount and a jurisdiction-appropriate tax note And the receipt total equals the sum of the two line items And the receipt includes the payment transactionId and payment method last4 where permitted And the donor profile timeline records an entry with both line items and their tax notes
Recurring Gift Upsell Handling and Explicit Consent
Given the donor has an active recurring plan when the upsell is shown Then the donor is presented with two explicit options: “Apply $X one-time now” and “Increase my recurring to $Y/month” And no option is preselected When the donor selects “Apply one-time now” and accepts Then only a one-time add-on is charged and the recurring plan amount and schedule remain unchanged And the receipt itemizes the one-time add-on When the donor selects “Increase recurring” and accepts Then the recurring amount is updated effective the next billing cycle and a confirmation message states the new amount and start date And a plan-change consent event with timestamp and variant metadata is recorded And no immediate add-on is charged unless “Apply one-time now” is also selected
Employer Match Eligibility and Acknowledgment for Add-On
Given the donor has an employer match profile on file or provides employer details during checkout When an add-on is accepted Then the system flags the add-on line item as match-eligible by default unless the designated fund is marked ineligible by the organization And the acknowledgement includes employer match instructions and reflects the add-on amount as eligible where applicable And the donor record stores matchEligibility per line item and employerId And the match export includes separate columns for primary gift and add-on eligibility flags and amounts
Restricted Funds and Campaign Designation for Add-On
Given the primary gift is designated to a restricted fund or campaign When an add-on is accepted Then the add-on inherits the same fund/campaign designation by default And if the campaign permits alternate designations, the donor may select a different allowed designation before accepting And the selected designation is displayed on the upsell prompt and on all receipts And finance/ledger exports tag the add-on with the correct fund, campaign, and restriction codes
Jurisdictional Solicitation Disclosures and Logging
Given the donor’s jurisdiction is determined from billing address with IP geolocation as fallback When the upsell prompt is displayed Then the prompt includes all required charitable solicitation disclosures for that jurisdiction before the donor can accept And the disclosure content versionId and jurisdiction code are recorded with the consent or decline event And for jurisdictions requiring explicit checkbox consent, the Accept action is disabled until the checkbox is checked And QA can verify presence of the disclosure text for at least one U.S. state with requirements and one non-U.S. jurisdiction
Add-On Only Refund/Reversal and Audit Trail Update
Given a payment with an accepted add-on has settled When an authorized staff user initiates a refund for the add-on only Then the system processes a partial refund equal to the add-on amount without affecting the primary gift And a corrected receipt/credit note is sent showing the refunded add-on and the unchanged primary gift And the donor record and ledger reflect the refund with a separate reversal entry for the add-on line item And the refund event captures timestamp, staff userId, reason code, transactionId, and links to the original consent event And the refund appears in the Compliance Export and Finance Export within 5 minutes

Match Minute

Automatically adds a time-bound match banner to SMS receipts when matching funds are live. Highlights “Your next $5 becomes $10 for 20 minutes,” then disables itself when the cap is reached. Creates authentic urgency and higher conversion without manual coordination.

Requirements

Live Match Campaign Configuration
"As an organizer, I want to configure time-bound matching rules and caps so that receipts automatically promote a live match without me babysitting it."
Description

Provide an admin UI and API to create and manage time-bound matching campaigns, including match ratio (e.g., 1:1), minimum/maximum eligible gift amounts, per-donor limits, overall fund cap, start/end schedule, eligible audience segments, supported channels (SMS receipts), default eligibility window duration (e.g., 20 minutes), and localized banner copy variants. Validate inputs, preview banners, and integrate with donation processing, SMS templating, and fund balance services to ensure accurate, coordinated activation without manual intervention.

Acceptance Criteria
Create Match Campaign via Admin UI
Given an authenticated admin with Manage Campaigns permission When the admin completes the New Match Campaign form with valid values for match ratio, min/max eligible gift, per-donor limit, overall fund cap, start/end schedule, eligible segments, channels=SMS receipts, default eligibility window duration, and localized banner copy And clicks Save Then the campaign is created And its state is set to Scheduled if now < start, Active if start <= now < end, or Ended if now >= end (creation blocked if Ended) And it appears in the campaign list with all fields persisted And the campaign is retrievable via the API with the same values and computed state
Validate Campaign Inputs and Constraints
Given the admin enters invalid values When they attempt to save Then the system blocks save and displays field-level errors for each violated rule, including: - match ratio must be a positive decimal (e.g., 1:1 → 1.0) - min eligible gift ≥ 0 and ≤ max eligible gift - per-donor limit ≥ 0 and ≥ min eligible gift and ≤ max eligible gift - overall fund cap > 0 and ≥ per-donor limit - start time < end time and both in the future or present - no overlapping Active/Scheduled campaign for the same segment+channel pair - at least one locale copy provided; tokens {amount}, {matched_amount}, {window_minutes} must be present - SMS banner copy per locale must not exceed the configured character limit and shows live character count And the API returns HTTP 400 with machine-readable error codes for the same violations
Scheduled Activation and Automatic Deactivation
Given a campaign is Scheduled When the current time reaches the start time Then the system transitions the campaign to Active within 10 seconds And banners become eligible for insertion into SMS receipts for qualifying donations Given a campaign is Active When the current time reaches the end time Then the system transitions the campaign to Ended within 10 seconds And no new banners are inserted and no new eligibility windows are started
Fund Cap and Per-Donor Limits Enforcement
Given a campaign is Active with remaining match funds When a qualifying donation is processed Then the matchable amount is min(donation within min/max, donor remaining limit, campaign remaining funds) And the fund balance service is decremented by the match amount atomically with donation processing And the SMS receipt reflects the match amount Given a donation would exceed the remaining campaign funds When it is processed Then only the remaining funds are matched and the campaign is marked Cap Reached and deactivated within 10 seconds And subsequent SMS receipts show no match banner Given a donor has reached their per-donor limit When they make another donation within the campaign window Then no match is applied and no banner is shown
Audience Segment and Channel Targeting
Given a campaign targets specific audience segments and channel=SMS receipts When a qualifying donation triggers a receipt for a donor in a targeted segment Then the SMS templating service injects the banner into the receipt Given a donor is not in any targeted segment or the channel is not SMS receipts When a donation triggers communications Then no banner is injected And other channels remain unaffected
Banner Copy Localization and Preview
Given localized banner copy variants with tokens {amount}, {matched_amount}, {window_minutes} When the admin selects a locale and sample inputs ($5, 1:1, 20 minutes) Then the preview renders “Your next $5 becomes $10 for 20 minutes” (or the locale-specific variant) And the preview shows character count and truncation warning if over limit Given a recipient’s locale is available When an SMS receipt is sent Then the banner uses the matching locale copy Given a recipient’s locale is unavailable When an SMS receipt is sent Then the banner falls back to the campaign’s default locale copy
Default Eligibility Window and Expiry Handling
Given a campaign defines a default eligibility window duration (e.g., 20 minutes) When an SMS receipt with a match banner is sent at time T0 Then the donor’s eligibility window is stored with expiry T0 + duration And donations received at time t where T0 ≤ t < expiry are eligible subject to all other limits And donations at or after expiry are not eligible Given the campaign ends or cap is reached before an individual window expires When subsequent donations occur Then no match is applied and no banner is included, and the state is reflected in APIs within 10 seconds
Real-time Eligibility Window & Countdown
"As a donor, I want a clear, accurate countdown for how long my next gift will be matched so that I can decide quickly and trust the offer."
Description

When an SMS receipt is generated and a match is live, create a recipient-specific eligibility window that starts at send time and lasts for the configured duration, but automatically shortens if the campaign ends sooner or the cap is exhausted. Expose a countdown token for inclusion in links/messages that enforces server-time expiry regardless of device clock or SMS latency. Handle retries and delivery uncertainty gracefully, and provide clear fallback messaging if the match ends before the donor acts.

Acceptance Criteria
Start Window at SMS Send (Live Match)
Given a match is active with configured duration D minutes, a campaign_end_at timestamp, and available cap > 0 And an SMS receipt with match banner is queued for recipient R at server time T_send When the message is successfully submitted to the SMS provider (HTTP 2xx) Then the system creates an eligibility window W for R with start_at = T_send and max_end_at = min(T_send + D, campaign_end_at) And W stores recipient_id, message_id, campaign_id, start_at, max_end_at, status = "active" And the SMS payload includes a countdown token scoped to W and campaign_id
Auto-Shorten on Cap Exhaustion or Early Campaign End
Given an active eligibility window W with max_end_at in the future And either the global match cap is exhausted at time T_cap or campaign_end_at is moved earlier to T_end < W.max_end_at When a recipient attempts to redeem W after min(T_cap, T_end) but before W.max_end_at Then W.effective_end_at is set to min(T_cap, T_end) And any redemption after W.effective_end_at is treated as expired (no match applied) And the countdown endpoint for W returns remaining_seconds = 0 and reason in {"cap_exhausted","campaign_ended"} And subsequent SMS sends automatically omit the match banner once the match is no longer active
Server-Time Countdown Enforcement in Links
Given a valid countdown token for window W with max_end_at and a live or recently ended campaign And the device clock may be skewed by ±S minutes and SMS delivery latency may be L minutes When the recipient taps the tokenized link at server time T_click Then the server computes remaining_seconds = max(0, min(W.max_end_at, campaign_end_at, cap_end_at) - T_click) And eligibility is granted only if remaining_seconds > 0 and available cap > 0 at T_click And client-side countdown renders values provided by the server; device time is not used for eligibility And any tampered or expired token is rejected (invalid signature or past expiry) and renders the expired fallback
Idempotent Send/Retry Handling
Given the SMS provider or our system retries send or delivery operations for the same message When multiple send attempts occur with the same message_id or idempotency key within the retry window Then exactly one eligibility window W is created for that message_id And repeated delivery status webhooks do not create additional windows And multiple link clicks with the same token do not create new windows or extend W And match application to a qualifying donation is atomic so that concurrent redemptions cannot overdraw the cap (cap never negative)
Fallback Messaging After Match Ends
Given a recipient follows the tokenized link or attempts a donation after W has expired due to time, cap exhaustion, or early campaign end When the server evaluates eligibility and finds the recipient ineligible Then the UI and API return a clear message indicating the match has ended and the ended_at timestamp and reason And the donation flow continues without applying any match multiplier or banner And analytics record an ineligible_attempt event with reason
Auditability and Metrics for Eligibility Windows
Given standard observability requirements When eligibility windows are created, shortened, expired, or redeemed Then an audit log entry is recorded with window_id, recipient_id, message_id, campaign_id, action, reason, and timestamps And metrics are emitted for windows_created, windows_active, windows_expired, windows_shortened_cap, redemptions_eligible, redemptions_ineligible_by_reason And an admin can retrieve a window by message_id to see start_at, effective_end_at, and exhaustion reason
SMS Receipt Banner Injection
"As a supporter receiving a receipt, I want a short, readable banner that tells me about the limited-time match and how to act so that I can give again easily."
Description

Extend the SMS templating engine to conditionally insert a compact match banner into receipts when eligibility is active. The banner includes concise copy (e.g., “Your next $5 becomes $10”), minutes remaining, and a tracked donate link tied to the countdown token. Enforce character limits with smart truncation and multilingual support, preserve deliverability across GSM-7/UCS-2, and ensure the opt-out footer remains present. Provide fallbacks that omit nonessential elements when messages would exceed configured segment budgets.

Acceptance Criteria
Banner Injection When Match Is Live and Receipt Is Sent
Given an eligible SMS receipt is generated during an active match window with remaining match funds When the receipt template is rendered Then a compact match banner is inserted immediately after the first receipt line And the banner text includes "Your next $X becomes $Y" where X and Y reflect the configured match ratio and currency formatting And the banner text includes "for N minutes" where N = floor((match_end_time - render_time)/60) and N ≥ 1 And the final message (including footer) fits within the configured segment budget after applying allowed fallbacks And the render log records banner_included=true with match_id and computed N
Auto-Disable Banner When Match Cap or Time Expires
Given the match cap has been reached or the match window has ended at render time or before send attempt When the SMS receipt is rendered or re-evaluated at send time Then the match banner is not included And the donate link is the standard non-match link And an exclusion_reason is logged as "cap_reached" or "time_expired" And queued messages re-check eligibility at send time and omit the banner if no longer eligible And cap enforcement is atomic so no receipt rendered after cap closure is marked match_live
Countdown Timer and Minutes Remaining Accuracy
Given a match with end time T_end When the receipt is rendered at T_render and enqueued for send at T_send Then the displayed minutes equals floor((T_end - T_render)/60) with a minimum display of 1 And the banner never displays 0 or negative minutes And if T_end - T_send < 60 seconds at send attempt, the banner is omitted and the standard receipt is sent And a clock-skew tolerance of ±5 seconds is permitted without showing an incorrect minute count
Tracked Donate Link Tied to Countdown Token
Given a match banner is included When generating the donate link in the banner Then a unique per-recipient token containing match_id and expiry is appended as a parameter and is preserved by link shortening And clicks are logged with token_id, recipient_id, match_id, and timestamp for attribution And clicks after expiry resolve to the standard donate experience and are logged with reason=expired And the shortened URL length does not exceed the configured short-link length budget And UTM parameters are included only if segment budget allows; otherwise they are omitted before removing the banner
Character Limits, Smart Truncation, and Segment Budget Fallbacks
Given the message may be encoded in GSM-7 or UCS-2 When computing segment count using standard concatenated SMS sizes (153 chars/segment GSM-7, 67 chars/segment UCS-2) Then the final message does not exceed the configured segment budget And fallbacks are applied in this order until within budget: (1) compress banner copy to short form (e.g., "Your next $X -> $Y", "for N min"), (2) remove UTM parameters from the link (preserve token), (3) remove the "for N min" phrase, (4) remove the banner entirely And the opt-out footer is never removed or altered by fallbacks And the applied fallback level is recorded in logs
Multilingual Support and GSM-7/UCS-2 Deliverability
Given the organization or recipient has a preferred locale When rendering the banner Then banner text, numbers, and currency formatting are localized for the selected locale And encoding selection preserves GSM-7 when possible; if UCS-2 is required, segment budgeting and fallbacks are recalculated accordingly And diacritics and non-Latin scripts render correctly without introducing unsupported or zero-width characters And end-to-end test sends succeed for at least one GSM-7 sample and one UCS-2 sample across multiple major carriers without delivery errors
Opt-out Footer Preservation and Compliance
Given all receipts must include the opt-out footer text per compliance When inserting the banner and applying any fallbacks Then the standard opt-out footer remains present verbatim (or localized equivalent) in the final message And if the message would exceed the segment budget, the banner is removed before any modification to the footer And a pre-send compliance check verifies footer presence; messages failing the check are blocked and logged with reason=missing_opt_out
Match Redemption & Attribution
"As a treasurer, I want donations made during the window to be automatically marked and reconciled as matched so that reports and receipts are accurate without manual work."
Description

Detect donations originating from banner links or occurring within an active eligibility window and automatically mark them as matched. Calculate the matched amount per rules, enforce per-donor and per-transaction caps, decrement the fund balance atomically, and generate matched line items on receipts and internal records. Tag donations for reporting and follow-up, and prevent abuse by limiting redemptions per window and applying cooldowns as configured.

Acceptance Criteria
Attribution from Match Banner Link
Given a match campaign is active and the SMS banner link contains a valid signed campaign token And the donor completes a donation via that link within the campaign's eligibility window When the payment is confirmed Then the donation is tagged matched:true with match_campaign_id set from the token And attribution_source = "banner_link" with token_id recorded And matched_amount = min(min(donation_amount, per_transaction_cap, per_donor_remaining) * match_rate, fund_remaining) And an audit event "match.applied" is recorded with computed fields
Time-Window Match Without Link
Given a match campaign is active and evaluated in the org-configured timezone And the donor donates via any channel without a campaign token When the payment timestamp is within [start_at, end_at] Then matched:true with attribution_source = "window" And matched_amount = min(min(donation_amount, per_transaction_cap, per_donor_remaining) * match_rate, fund_remaining) And if the timestamp is outside the window, matched:false with reason = "window_inactive"
Per-Donor and Per-Transaction Caps
Given per_transaction_cap, per_donor_cap, and match_rate are configured And the donor has a prior donor_eligible_matched_total of X for the campaign When the donor donates amount A Then matched_amount = min(min(A, per_transaction_cap, max(0, per_donor_cap - X)) * match_rate, fund_remaining) And donor_eligible_matched_total increases by matched_amount / match_rate And any portion of A above the caps is processed as unmatched
Atomic Fund Decrement & Concurrency Safety
Given fund_remaining = F in matching dollars at the moment of match application And two or more donations are processed concurrently When each donation attempts to apply matching Then matches are applied using atomic transactions so that the sum of matched_amounts deducted <= F And fund_remaining never becomes negative And if a donation requests more than the current fund_remaining, matched_amount = current fund_remaining and the remainder is unmatched And retries with the same payment_id are idempotent and do not change matched_amount or fund balance
Receipts and Internal Records Line Items
Given a donation is matched with matched_amount M > 0 When the receipt is generated Then the receipt includes a separate line item labeled "Match" with amount = M, clearly distinct from the donor amount, and donor total remains the donor's contribution amount And internal records create a ledger entry type = "match" amount = M linked to donation_id, fund_id, and match_campaign_id And the donation record is tagged matched:true, match_campaign_id, attribution_source, match_amount = M, and these fields are available in exports and API
Redemption Limits and Cooldowns
Given redemption_limit_per_window and cooldown_minutes are configured for the campaign And the donor has already redeemed R matched transactions in the current window When the donor attempts additional donations Then only transactions resulting in matched_amount > 0 count toward the redemption limit And no more than redemption_limit_per_window transactions for that donor are matched in the window And any donation within cooldown_minutes of the last matched donation is not matched and is tagged matched:false with reason = "cooldown" and next_eligible_at set
Auto-Disable on Fund Depletion
Given a match campaign's fund_remaining reaches 0 When subsequent receipts are generated and donation pages are rendered Then no match banner or indicators are shown and new donations are not matched (matched:false, reason = "fund_depleted") And the campaign status transitions to "depleted" and remains so until re-funded by an admin And this change propagates across SMS banners and donation flows within 60 seconds
Cap Management & Concurrency Safeguards
"As an engineer, I want robust cap tracking that prevents overspending during spikes so that the match cannot exceed the committed funds."
Description

Implement a centralized fund balance service with atomic decrements, optimistic locking, and idempotency keys to handle simultaneous redemptions during spikes. Provide a low-latency API for “is match live” checks, soft-stop thresholds for warning messages, and hard-stop auto-disable when caps are reached. Include recovery routines for rollback on failed transactions and comprehensive audit logs for all balance changes.

Acceptance Criteria
Atomic Decrement Under Peak Concurrency
Given a fund with initial_match_cents = 100000 and per-redemption match amount = 100 cents And 5,000 concurrent valid redemption requests each with a unique idempotency key When all requests are processed Then exactly 1000 redemptions succeed with a match and 4000 are declined due to cap And remaining_match_cents = 0 and never goes below 0 at any time And each successful redemption decrements the balance exactly once (no duplicates) And p95 redemption endpoint latency <= 250 ms and p99 <= 500 ms at 1000 RPS in staging
Idempotent Redemption with Retries
Given a redemption request identified by idempotency_key K, amount A, and fund F When the same request (same K, A, F) is retried up to 10 times within the idempotency TTL Then the service returns the identical response and reference for all retries and the balance decrements at most once And if a retry changes any parameter (A or F) while reusing K, the service returns 409 Idempotency-Key-Mismatch and does not decrement And idempotency TTL is configurable (default 24h, min 1h, max 7d)
Optimistic Lock Conflict Resolution
Given concurrent updates cause a version conflict on a fund balance row When a decrement attempt detects the conflict Then the service retries with exponential backoff and jitter up to 5 attempts within 300 ms total And if still conflicting after retries, it returns 409 Conflict without applying any decrement And metrics report lock_conflicts_total and lock_conflicts_resolved, with >= 99% resolved under 1000 RPS in staging tests
Low-Latency "Is Match Live" API
Given the isMatchLive endpoint is queried for fund F under typical and peak load When requests are made continuously for 5 minutes at 500 RPS in staging Then p95 latency <= 50 ms and p99 <= 100 ms from the service edge And responses include: live (bool), remaining_cents (int), soft_stop (bool), hard_stop (bool), window_ends_at (ISO-8601) And flags reflect the authoritative balance state with max staleness <= 1s And the endpoint is idempotent and safe (HTTP GET)
Soft-Stop Threshold Warnings
Given a fund with soft_stop_threshold T (in cents) When remaining_match_cents <= T and > 0 Then isMatchLive returns soft_stop = true and live = true And banner text switches to the "ending soon" variant within 5 seconds And when remaining_match_cents rises above T, soft_stop returns to false within 5 seconds
Hard-Stop Auto-Disable on Cap Reach
Given remaining_match_cents = R When a decrement request for amount A arrives such that A > R or R = 0 Then the request is declined with MATCH_ENDED (HTTP 409 or 422) and no decrement occurs And isMatchLive returns live = false and hard_stop = true within 1 second of cap reach And the match banner auto-disables within 1 second and is not shown to subsequent users
Recovery, Rollback, and Audit Logging
Given a decrement is reserved but the transaction fails before commit When the failure is detected Then the reserved amount is rolled back and remaining_match_cents is restored within 60 seconds And 100% of balance changes (attempted, committed, rolled back) produce immutable audit records with: timestamp, fund_id, old_balance, new_balance, delta, idempotency_key, correlation_id, actor, outcome And an audit query by correlation_id returns the complete event chain in order And a periodic recovery job reconciles any pending or orphaned reservations to a consistent state on each run
Admin Status Panel & Alerts
"As an organizer, I want a live view and alerts for the match so that I can adjust strategy and avoid surprises."
Description

Add a dashboard module that shows live campaign status, including remaining funds, conversion rate from banners, average time-to-gift, banner send volume, and projected time to cap. Provide configurable alerts via SMS/email for thresholds (e.g., 80% funds used), activation/deactivation events, and anomalies. Allow pausing/resuming campaigns, editing copy, and extending duration within safe constraints, with audit trails for changes.

Acceptance Criteria
Live Metrics Accuracy & Refresh
Given a live Match Minute campaign and the Admin Status Panel is open When new donations are processed or time elapses Then remaining funds, conversion rate from banners, average time-to-gift, banner send volume, and projected time-to-cap are displayed with clear labels and units And each metric shows a last-updated timestamp with data lag ≤ 15 seconds while activity is occurring And metrics recalculate at least every 10 seconds while the tab is active And after a $50 matched donation posts, remaining funds decrease by $50 within 15 seconds and banner send volume increments by 1 And projected time-to-cap backtested over the last 60 minutes has mean absolute percentage error ≤ 10% And when no active match is running, the panel displays "No active match" and disables match-only controls
Threshold Alerts Configuration & Delivery
Given an admin configures an "80% funds used" threshold with SMS and Email recipients When funds used crosses 80% for the first time in a campaign Then exactly one SMS and one Email alert are sent to the configured recipients within 60 seconds And additional crossings within a 60-minute dedup window do not send duplicates unless the admin resets the alert And alerts contain campaign name, current funds used %, remaining funds, timestamp, and a link to the panel And invalid or unsubscribed recipients are skipped; delivery results (Sent/Failed) are logged and visible in the panel And a "Send test" action sends a test alert with a [TEST] prefix within 30 seconds
Activation/Deactivation Event Alerts
Given a Match Minute campaign is scheduled or manually started When the campaign activates Then an activation alert is sent to configured channels within 60 seconds including campaign name, start time, starting funds, and link to the panel Given a campaign reaches cap or end time When the campaign deactivates Then a deactivation alert is sent within 60 seconds including campaign name, deactivation reason (Cap reached/Time ended/Manual pause), funds used, and link And exactly one alert is sent per event with no duplicates
Pause/Resume Campaign Control
Given a running campaign on the Admin Status Panel When an admin with permission clicks Pause and confirms Then the campaign state changes to Paused within 5 seconds in the UI and in new processing And match banners are not appended to any SMS receipts generated ≥ 30 seconds after the pause And an audit trail entry is recorded with admin ID, timestamp, reason (optional), and before/after state Given a paused campaign When the admin clicks Resume Then match banners resume for receipts generated ≥ 30 seconds after resume and the state shows Running And the resume action is recorded in the audit trail
Edit Banner Copy With Validation & Preview
Given an admin opens the banner copy editor from the panel When editing the SMS banner text Then a live character counter is shown and save is blocked if the rendered text would exceed 160 GSM-7 characters (or 70 UCS-2), including variables And only supported variables {donationAmount}, {matchAmount}, {minutesRemaining} are allowed; unsupported tokens block save with an error message And a preview renders with sample data before saving When the admin saves valid changes Then the new copy is used for subsequent receipts within 30 seconds and an audit trail entry captures the diff, admin ID, and timestamp When the admin cancels, Then no changes are persisted
Extend Duration Within Safe Constraints
Given an active campaign with remaining funds > 0 When an admin attempts to extend the end time Then the UI enforces a maximum extension of 60 minutes per change and prevents setting an end time past 23:59 in the org’s timezone And the system blocks the change if projected spend to the new end time would exceed the remaining cap, showing a clear explanation And a successful extension updates the scheduler immediately, reflects in the panel within 5 seconds, sends an informational alert to configured recipients, and records an audit entry with before/after times and admin ID
Anomaly Detection & Alerting
Given anomaly rules are enabled When banner send volume over any 5-minute window exceeds 3× the 60-minute rolling average with ≥ 20 sends Or when conversion rate from banners over the last 15 minutes falls below 50% of the 60-minute baseline with ≥ 50 sends Then an anomaly is created and an alert is delivered to configured channels within 5 minutes including metric snapshots, rule triggered, and a link to the panel And the panel shows an Active Anomaly badge with details and an Acknowledge action And acknowledgments suppress repeat alerts for that rule for 30 minutes while the anomaly persists And all anomalies and acknowledgments are logged in the audit trail
Compliance, Throttling, and Accessibility
"As a program manager, I want compliant, respectful outreach that avoids spamming and remains accessible so that our supporters trust us and carriers don’t filter messages."
Description

Ensure SMS compliance by sending banners only to opted-in contacts, honoring quiet hours and regional regulations, and always including opt-out instructions. Implement throttling to limit banner exposures per contact and per campaign (e.g., one banner per 24 hours) and suppress messaging for recent non-responders. Enforce readable copy, localization, and accessible link presentation on mobile, with graceful degradation when multi-segment messages would exceed budget or carrier limits.

Acceptance Criteria
Consent and Regional Compliance for Match Banners
Given contact.sms_opt_in=true and the regional compliance ruleset allows promotional overlays, When an SMS receipt is generated while a match is live, Then append the Match Minute banner and include exactly one opt-out instruction line containing 'Reply STOP to opt out'. Given contact.sms_opt_in=false, When an SMS receipt would be generated, Then do not send the SMS, do not append a banner, and log suppression_reason='no_sms_consent'. Given contact.sms_opt_in=true but the contact's region prohibits promotional overlays on receipts, When an SMS receipt is generated, Then do not append the banner and log suppression_reason='region_policy'. Given a bannered receipt is sent, When the event is persisted, Then write an audit record with contact_id, campaign_id, banner_applied=true, region_code, ruleset_version, and timestamp.
Quiet Hours Enforcement by Region
Given the contact's local time (derived from timezone or geo) is within configured quiet hours for their region, When a receipt event occurs, Then queue the SMS for the earliest allowed local send time and set delivery_hold='quiet_hours'. Given a queued receipt reaches its release time and the match window has expired or the match cap was reached, When the SMS is sent, Then send the standard receipt without the Match Minute banner. Given org policy suppress_during_quiet_hours=true for the region, When a receipt event occurs during quiet hours, Then do not queue or send the SMS and log suppression_reason='quiet_hours'. Given contact.dnd=true, When a receipt event occurs, Then do not send an SMS and log suppression_reason='contact_dnd'.
Per-Contact and Per-Campaign Throttling
Rule: Enforce a per-contact exposure limit of <=1 Match Minute banner across all campaigns in any rolling 24-hour window. Rule: Enforce a per-campaign exposure limit of <=1 Match Minute banner per contact for that campaign in any rolling 24-hour window. Rule: If either limit would be exceeded for the pending receipt, suppress the banner, send the standard receipt, and log suppression_reason in {'throttled_contact_24h','throttled_campaign_24h'}. Rule: Throttle evaluation and decisioning occur atomically at message composition time and are persisted with contact_id, campaign_id, scope (contact|campaign), window_hours=24, and timestamp.
Non-Responder Suppression Window
Rule: Define a recent non-responder as a contact with 0 replies and 0 tracked link clicks across the last 5 outreach SMS within the previous 7 days. Given a contact meets the non-responder definition, When an SMS receipt is generated, Then do not append the Match Minute banner and log suppression_reason='non_responder'. Given a contact exits the non-responder state via any reply or tracked link click, When the next SMS receipt is generated, Then re-evaluate banner eligibility under standard rules (consent, quiet hours, throttling) and apply accordingly.
Accessible Copy and Link Presentation
Rule: Banner copy length is between 10 and 120 GSM-7 characters (excluding the opt-out line), uses sentence case, and avoids all-caps except the carrier keyword 'STOP'. Rule: Banner includes at most one HTTPS link using a branded domain; the link is preceded by an action label (e.g., 'Tap: ') and is separated by spaces so it is tap-detectable on iOS and Android default SMS apps. Rule: The opt-out instruction is present as the final line of the message and reads exactly 'Reply STOP to opt out'. Rule: English banner text scores <= Grade 8 on Flesch–Kincaid or approved readability metric; non-English variants use an equivalent internal readability check. Rule: Message content is limited to GSM-7 where possible; if UCS-2 would be required due to characters, the system evaluates segment/budget limits per graceful degradation rules before sending.
Localization of Banner and Opt-Out
Given a contact has locale and region (e.g., es-MX), When an SMS receipt is generated while a match is live, Then render the banner and opt-out instruction in the contact's locale using localized strings and formats. Rule: Currency, numbers, and duration units in the banner are formatted per the contact's locale (e.g., currency symbol/placement, pluralization for minutes). Rule: If a locale-specific translation is unavailable, fall back to English (en-US) and record locale_fallback=true in the audit log. Rule: The opt-out instruction uses the carrier-approved keyword(s) for the contact's region and remains present after localization.
Graceful Degradation for Segment and Budget Limits
Given a configured max_segments_per_message and/or per-message cost budget, When appending the banner would cause the message to exceed the limit, Then prefer delivery of the standard receipt by removing the banner and log degradation_reason='segment_or_budget_cap'. Rule: If a short-form banner variant exists, attempt it first; if still over the limit, drop the banner entirely before suppressing the receipt. Rule: Degradation decisions are deterministic and recorded with message_id, estimated_segments_before, estimated_segments_after, budget_before, budget_after, and chosen_variant. Rule: Under no circumstances may the system send a message exceeding the configured segment or budget cap.

HeatBurst Meter

Turns the live thermometer into moment-based milestones like “5 donors to 75%” and celebrates when someone pushes the bar past a marker. The receipt instantly reflects the impact of their tap, reinforcing momentum and encouraging quick follow-on gifts.

Requirements

Milestone Logic Engine
"As a campaign organizer, I want milestones that automatically reflect our current progress so that I can motivate supporters with specific, actionable goals in the moment."
Description

Define and compute dynamic, moment-based milestones from live campaign metrics (e.g., “5 donors to 75%,” “$250 to next marker,” or “2 shifts to fill Saturday 10–12”). Supports multiple units (donors, dollars, shifts, hours) and campaign scopes, with configurable rounding, thresholds, and time windows. Exposes a service that evaluates current progress and returns the next milestone plus delta-to-go, recalculated on each qualifying event. Handles edge cases such as simultaneous gifts, refunds, pledge reversals, and offline sync to prevent double counting. Emits normalized milestone-crossed events consumed by the Impact Board, receipts, and notifications.

Acceptance Criteria
Next milestone calculation for donors to 75%
Given a donation campaign with goal=200 donors and currentDonors=145, markers at 25%, 50%, 75%, 100%, and rounding=ceil When the milestone service is evaluated with unit="donors" and scope=campaignId at time t0 Then the response includes milestone.label="5 donors to 75%", milestone.targetValue=150, deltaToGo=5, unit="donors", scopeId=campaignId, markerPercent=0.75, recalculatedAt=t0 And the response is recomputed and updated on each qualifying event (donation.created, donation.refunded, pledge.reversed, offline.sync.applied)
Simultaneous gifts do not double-count and emit one crossing event
Given currentDollars=900, goalDollars=1000, nextMarker=1000, and two donation.created events of $100 each arrive near-simultaneously When both events are processed by the engine Then currentDollars becomes 1100 And exactly one normalized milestone-crossed event is emitted for marker=1000 with idempotencyKey derived from (scope, unit, marker, crossingIndex) And no duplicate milestone-crossed events are emitted for the same marker And the crossing timestamp is recorded as the earlier of the two event timestamps
Refunds and pledge reversals retroactively adjust milestones
Given a campaign that has crossed the $1000 marker with currentDollars=1100 And a refund event of $150 is received for a prior donation When the refund is processed Then currentDollars becomes 950 And a milestone.regressed event is emitted referencing marker=1000 and scope=campaignId And the next milestone evaluation returns label="$50 to $1000" with targetValue=1000 and deltaToGo=50 And previously crossed markers are not re-celebrated until re-crossed
Shift-based milestone for Saturday 10–12
Given a volunteer shift group for 2025-08-16 10:00–12:00 requires 8 slots and assignedShifts=6 When the milestone service is evaluated with unit="shifts" and scope=shiftGroupId Then the response includes milestone.label="2 shifts to fill Saturday 10–12", targetValue=8, deltaToGo=2, unit="shifts", scopeId=shiftGroupId And when two assignment.created events fill the last two slots Then exactly one milestone-crossed event is emitted for this shift group with filledCount=8 and windowStart/End for the shift timebox
Time-windowed dollars with configurable rounding
Given a rolling24h window configured with markers every $250 and rounding.display=ceil to $1 And currentWindowDollars=$237 at time t0 When the milestone service is evaluated with unit="dollars" and window="rolling24h" Then the response includes milestone.label="$13 to next $250 window marker", targetValue=250, deltaToGo=13, window="rolling24h" And when a $20 donation occurs at t1 within the window Then a single milestone-crossed event is emitted for marker=$250 And the next evaluation returns targetValue=500 with deltaToGo based on the updated rolling total
Multiple campaign scopes resolution
Given a parent campaign P with subcampaigns S1 and S2 where donors and dollars are tracked per child and aggregated to P When the milestone service is evaluated with scope=P and unit="donors" Then the computed current and deltaToGo include the sum of S1 and S2 without double counting shared contributors per scope rules And milestone-crossed at the parent emits an event with scopeId=P and includedChildScopes=[S1,S2] And evaluating S1 or S2 individually returns independent results unaffected by the parent crossing
Offline sync prevents double counting and preserves order
Given a field device records three offline donations with clientIds [c1,c2,c2] and eventTimes [t1<t2<t3] And one of these (c2) was already synced earlier from another device When the batch is uploaded and the engine ingests events with idempotency keys derived from clientId+amount+donor+timestamp Then only unique contributions (c1 and the first c2) are applied to metrics And milestone evaluation uses original eventTimes to determine crossing order And no duplicate milestone-crossed events are emitted for the same marker due to offline replays
Real-time Thermometer Sync
"As a donor on my phone, I want to see the thermometer respond instantly to my tap so that I feel my contribution matters right away."
Description

Stream live progress updates to the Impact Board and mobile clients with sub-second latency so the HeatBurst meter reacts immediately to donations, signups, and shift assignments. Provide a reliable event pipeline (e.g., WebSocket/SSE with fallback) with at-least-once delivery, idempotent handlers, and backpressure control. Include visual diffing to animate from prior value to new value and prefetch the next milestone marker. Ensure performance targets on low-end mobile (≤100ms frame budget) and degraded networks, with reconnection and offline buffering.

Acceptance Criteria
Sub-Second E2E Update to HeatBurst Meter
Given a donation, signup, or shift assignment event for Campaign X is accepted by the API with event_id and sequence When the event is published to the realtime stream Then the Impact Board and all connected mobile clients subscribed to Campaign X update the HeatBurst meter within 800 ms p95 and 1500 ms p99 end-to-end without manual refresh And the on-screen value transitions from the previous value to the new value without displaying intermediate incorrect totals And the client records measured latency per event for verification
Transport Selection and Seamless Fallback
Given a client starts a realtime session When it cannot establish a WebSocket within 3 s or receives a 4xx/5xx Then it attempts Server-Sent Events within 500 ms without user action; if SSE fails within 3 s, it falls back to long-polling with 10 s backoff And when connectivity improves, the client upgrades back to WebSocket on next reconnect without losing or duplicating events And transport changes do not interrupt UI animations and maintain at-least-once delivery using the last acknowledged sequence or resume token
At-Least-Once Delivery with Idempotent Handling
Given the stream delivers duplicate or out-of-order events with the same event_id or a lower/equal sequence number When the client processes them Then the HeatBurst total is applied exactly once, remains monotonic non-decreasing, and no duplicate milestone celebrations fire And on reconnect with a resume token/last_sequence, the server replays from last_sequence−overlap, and the client de-duplicates using event_id/sequence And auditing after a 10,000-event fuzz test shows zero net over-count and zero missed increments
Visual Diff Animation and Next-Marker Prefetch
Given a prior displayed value V0 and a new value V1 When an event arrives Then the meter animates from V0 to V1 within 300–600 ms while keeping main-thread work ≤100 ms per frame And the next milestone marker assets (copy, amount, celebration) are prefetched within 200 ms of the update with ≥95% cache hit ratio during a 30 s burst And if V1 crosses a milestone boundary, a single celebration animation plays, and the donor receipt view includes a line noting the milestone crossed within 1 s of payment confirmation
Low-End Mobile Performance Budget
Given a low-end Android device (4× Cortex-A53 class, 2 GB RAM) on a 3G network When receiving up to 10 progress events per second for 10 seconds Then UI long tasks on the main thread stay ≤100 ms p99, dropped frames ≤5%, and memory growth ≤25 MB during the burst, with memory returning to within 10 MB of baseline within 5 s And CPU usage attributable to the client stays ≤1 core average during the burst And backgrounded clients defer animations but resync state on resume within 1 s
Reconnect, Offline Buffering, and State Resync
Given the client loses connectivity for up to 15 minutes When connectivity is restored Then it reconnects within 3 s using a resume token and reaches a caught-up state (no pending deltas) within 2 s p95 And locally queued outbound actions (e.g., pledges or signups captured offline) are sent with idempotency keys and reflected in the meter within 1 s of server acceptance; pending items are labeled and do not trigger celebrations until confirmed And the UI displays Reconnecting within 500 ms of disconnect and clears it upon caught up
Cross-Client Consistency and Ordering
Given at least five mobile clients and the Impact Board subscribed to the same campaign When a burst of 100 progress events occurs over 10 seconds Then all clients converge to the identical total within 2 s of the last event p95 with consistent rounding/formatting And per-campaign ordering is preserved by sequence number; no client displays a lower total after a higher total has been shown And milestone crossings are detected consistently: at most one celebration per crossing per client, and all clients show the same milestone within 2 s
Celebration UI & Haptics
"As a supporter, I want a quick, delightful celebration when I trigger a milestone so that I feel recognized and motivated to keep helping."
Description

Render lightweight, celebratory animations and haptic feedback when a user pushes the meter past a milestone, attributing the moment to them by name or anonymous label per privacy settings. Provide accessible, low-distraction effects (reduced motion support, screen reader announcements, color-contrast safe) and small-footprint assets for quick load. Include variants for donors, volunteers, and staff dashboards, and a silent mode for events and meetings. Track animation impressions for analytics and allow A/B variants under a feature flag.

Acceptance Criteria
Milestone Crossing Celebration Trigger
- Given an authenticated user completes an action that updates the HeatBurst meter, When the update causes the meter to cross a defined milestone threshold (e.g., 25%, 50%, 75%, 100%), Then a celebration animation begins within 300 ms of receiving the meter update confirmation. - Given the meter update does not cross a milestone, When the action completes, Then no celebration animation plays. - Given multiple milestones are crossed by a single update, When the celebration displays, Then only one condensed celebration is shown referencing the highest milestone crossed and no more than one animation plays per update. - Given the celebration animation plays, Then its duration is between 0.8 s and 1.2 s and it does not obscure primary controls for more than 1.5 s.
Donor Attribution and Privacy Labeling
- Given the user's privacy setting is Public Name, When the celebration displays, Then it attributes using the format "FirstName L." and never shows the full last name. - Given the user's privacy setting is Organization-Defined Anonymous Label, When the celebration displays, Then it uses that label verbatim. - Given the user's privacy setting is Fully Anonymous, When the celebration displays, Then it uses "Anonymous" and does not expose any PII in UI or logs. - Given attribution data is unavailable, When the celebration displays, Then the fallback label "Guest" is used. - Given screen reader announcements are enabled, When the celebration is announced, Then the same label that is shown visually is announced.
Accessibility: Reduced Motion, Screen Reader, and Contrast
- Given the OS/browser setting prefers-reduced-motion is enabled or the app Reduced Motion toggle is on, When the celebration would play, Then no motion animation plays; a static celebratory badge is shown instead and the sequence completes within 500 ms. - Given a screen reader is active, When the celebration triggers, Then an aria-live="polite" announcement is made within 500 ms with text including the milestone (e.g., "You pushed us past 75%"), respecting privacy labels, and focus is not moved. - Given the celebratory visuals include text or icons, Then all foreground/background combinations meet WCAG 2.1 AA contrast (>= 4.5:1 for normal text and >= 3:1 for large text/icons). - Given any animation plays, Then no flashing occurs above 3 Hz and total flashes do not exceed seizure thresholds.
Haptics and Silent Mode Compliance
- Given the device supports haptics and OS haptics are enabled, When a milestone celebration triggers, Then a single short haptic pulse (<= 40 ms) is emitted once. - Given the app Silent Mode is enabled or the device is in Do Not Disturb, When a celebration triggers, Then no haptic feedback is emitted and no sound is played, and the visual effect is reduced to the static badge. - Given multiple celebrations could occur within 5 seconds, Then haptic feedback is rate-limited to at most one pulse per 5 seconds per device. - Given OS haptics are disabled, When a celebration triggers, Then no haptic pulse is emitted.
Performance and Asset Footprint
- Given a cold session, When the first celebration occurs, Then total additional assets downloaded for celebration are <= 30 KB gzipped and are lazy-loaded. - Given the celebration renders, Then the main thread is not blocked for more than 50 ms and the average frame rate remains >= 50 FPS on a mid-tier device (e.g., Pixel 5 or iPhone 11). - Given assets fail to load, When a celebration is triggered, Then a minimal fallback (static badge) displays within 300 ms and asset retries do not exceed two attempts per session.
Role-Based UI Variants
- Given the viewer is a Donor, When a milestone is crossed by their donation, Then donor-specific copy and visuals are used. - Given the viewer is a Volunteer, When their signup or action crosses a milestone, Then volunteer-specific copy and visuals are used. - Given the viewer is Staff, When their assignment or action crosses a milestone, Then staff-specific copy and visuals are used. - Given the role cannot be resolved, When a celebration needs to render, Then the donor variant is used as default and the event is logged with role=unknown. - Given deep links into different dashboards, When a celebration triggers, Then the variant respects the current dashboard role context.
Analytics Impressions and A/B Variant Flagging
- Given a celebration is shown (including static fallback), When it becomes visible, Then an analytics event "celebration_impression" is sent once per trigger with properties: user_id (hashed), role, milestone_id, crossed_from, crossed_to, variant_id, privacy_label_type, reduced_motion, haptics_enabled, timestamp. - Given rapid repeated triggers within 10 seconds, When celebrations occur, Then impressions are deduplicated by user_id + milestone_id + variant_id within a 10-second window. - Given the feature flag HeatBurstCelebrationVariants is OFF, When a milestone is crossed, Then no celebration UI/haptics are shown and no impression events are sent. - Given the feature flag HeatBurstCelebrationVariants is ON with variants A/B, When a milestone is crossed, Then the correct variant assets/copy are loaded per assignment and analytics include variant_id; if assignment fails, the control variant is used.
Instant Impact Receipt
"As a donor, I want my receipt to reflect the exact impact of my gift so that I feel confident and excited to share it with friends."
Description

Generate and send immediate receipts that contextualize the user’s action with milestone language (e.g., “You pushed us past 75%—thank you!”) across channels (email, SMS, in-app). Use a template system with dynamic tags for campaign name, milestone crossed, delta closed, and next target, and support localization and compliance (tax language, opt-out, CAN-SPAM). Include retry and bounce handling, and write receipt events back to the contact record. Surface prominent social/share CTAs to amplify momentum while respecting privacy preferences.

Acceptance Criteria
Milestone Crossed Celebratory Receipt
Given a campaign with milestones (e.g., 50%, 75%, 100%) and a confirmed payment event that moves progress from below to at/above a milestone When the payment success webhook is received Then the generated receipt copy contains celebratory milestone language including the exact crossed milestone percent (e.g., “You pushed us past 75%—thank you!”) and the correct delta_closed value And Then the copy includes next_target percent if campaign < 100%; if campaign >= 100%, a completion message is shown instead of next target And Then share CTAs (minimum two: Copy Link plus ≥1 network) are present and visible without scrolling on a 375x667 viewport in email and in-app, and appear as the final line in SMS And Then the share payload contains only campaign-level details (no donor name, email, phone, or amount) when privacy_preference = restricted And Then the SMS variant is ≤160 GSM-7 characters (or ≤70 UCS-2) including required compliance text; if longer, it is truncated at a word boundary while preserving compliance text
Approaching Next Milestone Receipt Copy
Given a confirmed payment event that does not cross any milestone When the receipt is generated Then the copy communicates momentum using the configured unit: “{X} donors to {next_target_percent}%” if momentum_unit=donors, or “{currency}{Z} to {next_target_percent}%” if momentum_unit=amount And Then X is the ceiling of donors needed to reach the next_target; Z is the remaining amount to next_target with locale-appropriate currency formatting And Then delta_closed and next_target_percent tags reflect the current post-donation state and never display negative or zero deltas And Then no milestone_crossed celebratory text appears
Immediate Multi-Channel Delivery SLA
Given a contact with channel preferences (email, SMS, in-app) When a payment success webhook is received Then a receipt payload is generated within ≤2 seconds and queued to all enabled channels within ≤5 seconds And Then the in-app receipt view is available within ≤3 seconds for active sessions and within ≤5 seconds after next login for inactive sessions And Then at least one opted-in channel receives a delivery confirmation within ≤15 seconds for 95% of test sends in staging And Then if no outbound channels are opted-in, the receipt is stored and visible in the contact’s timeline and in-app inbox without outbound sends
Template Engine Dynamic Tags and Content Rules
Given channel-specific templates containing tags {{campaign_name}}, {{milestone_crossed_percent}}, {{delta_closed}}, and {{next_target_percent}} When rendering a receipt event Then all referenced tags are populated from the event context with correct data types and formatting (percent with no more than 1 decimal, currency per locale) And Then unsupported or null tags are omitted without leaking raw tag literals into the output And Then conditional sections (e.g., if milestone_crossed) render correctly for both true and false cases across email, SMS, and in-app And Then rendering failures are logged and the message is not sent with a broken template
Localization and Fallback Behavior
Given contact.preferred_locale and organization.default_locale When generating receipts Then copy is rendered in contact.preferred_locale if a localized template exists; otherwise falls back to organization.default_locale; otherwise to en-US And Then number, percent, date, and currency formats follow the selected locale And Then right-to-left locales render with correct direction in email and in-app And Then SMS opt-out text (e.g., STOP/HELP) and legal lines are localized to the message language
Compliance, Consent, and Opt-out Enforcement
Given compliance requirements for email (CAN-SPAM) and SMS When sending receipts Then email includes organization legal name, postal address, and a functional unsubscribe link And Then SMS includes localized opt-out instructions (e.g., “Reply STOP to opt out”) and honors STOP/UNSTOP/HELP keywords And Then no message is sent on channels where the contact is unsubscribed, blocked, or lacks explicit consent And Then tax-deductible donations include required tax receipt language (amount, date, org name, EIN where applicable); non-deductible acknowledgements omit tax statements
Retry, Bounce, Failover, and Idempotent Write-Back
Given outbound send attempts and contact record logging When a transient send failure occurs (timeouts, 5xx) Then the system retries up to 3 times with exponential backoff (≈30s, 2m, 10m) before marking the attempt failed And When a permanent failure occurs (hard bounce, 4xx, spam complaint) Then no further retries are attempted and the channel status is updated (e.g., email_status=bounced/complained) to suppress future sends And Then if one channel fails and another is enabled, a failover send is attempted on the alternate channel And Then each receipt is assigned a unique receipt_id; duplicate payment events with the same transaction_id do not create duplicate sends or logs (idempotent processing) And Then a receipt event is written to the contact record with fields: receipt_id, transaction_id, campaign_id/name, milestone_crossed flag, milestone_percent, delta_closed, next_target_percent, channels_attempted, channels_delivered, timestamps, locale, template_version, compliance_flags; the event appears in the timeline within ≤5 seconds
Momentum Alerts
"As an organizer, I want automated alerts around key moments so that I can turn momentum into more gifts and signups without manual effort."
Description

Send timely nudges to opted-in supporters when a campaign is close to a milestone (e.g., “3 donors to go”) and follow-up bursts when a milestone is crossed, with frequency caps and quiet hours. Target by audience segments (past donors, scheduled volunteers, nearby contacts) and channels (push, SMS, email), and automatically expire alerts when conditions change. Provide admin controls for copy, thresholds, and cadence, and ensure deliverability and unsubscribe flows meet compliance and privacy requirements.

Acceptance Criteria
Proximity Nudge Triggering at 3 Donors to Milestone
Given a campaign milestone of 100 donors and current donor count is 97 And at least one supporter is opted in to alerts When the donor count first enters the proximity band defined as <=3 donors to the milestone (e.g., count becomes 97, 98, or 99) Then the system generates exactly one proximity nudge event with dynamic copy resolving to the correct remaining donor count (e.g., "3 donors to go") And the same proximity nudge is not generated again for that campaign until the count exits the band (<97) and re-enters it And proximity nudge generation timestamps and inputs are logged for audit And if the current time falls within a recipient’s quiet hours window, the event remains eligible but is deferred to the next allowed sending window
Milestone Crossed Burst Notifications
Given a campaign milestone of 100 donors and current donor count is 99 When a qualifying action increases the donor count to 100 or more Then a single milestone crossed burst is queued for delivery within 30 seconds And the burst copy includes resolved milestone label and contributor impact tokens when available And duplicate bursts for the same milestone crossing are not sent And deliveries respect per-recipient frequency caps and quiet hours And all send attempts and outcomes are logged with message IDs per channel
Audience Segmentation Targeting
Given the admin selects audience segments Past Donors and Nearby Contacts (within 10 miles) for a proximity nudge When the system prepares the recipient list for the event Then it computes the union of the selected segments and de-duplicates contacts across segments And it excludes contacts without valid consent for the chosen channels And it displays a targetable count before dispatch And it sends only to the computed target set and stores the targeting snapshot with segment criteria and counts
Multi-Channel Delivery with Fallback and Rate Limits
Given channel priority is configured as Push > SMS > Email and a contact has Push token and SMS opt-in When an alert is dispatched to that contact Then the system attempts delivery via Push first And if Push returns a permanent failure within 5 minutes, it attempts SMS once And if no consent exists for a channel, that channel is never used And per-contact quiet hours are enforced by deferring delivery to the next allowed window in the contact’s local timezone And a per-contact cadence cap (e.g., max 2 alerts per 24 hours) is enforced; over-cap contacts are skipped and counted And per-channel delivery status (success, soft fail, hard fail), provider response code, and timestamps are recorded
Auto-Expiration of Stale Alerts on Condition Change
Given a "3 donors to go" proximity nudge is scheduled for 6:00 PM And before 6:00 PM the milestone is reached or surpassed When the scheduler evaluates pending alerts Then it cancels the pending nudge as expired and logs the reason (milestone no longer applicable) And no message is sent for the expired alert And if the campaign later re-enters a valid proximity state, a new nudge may be generated subject to cadence rules And expired alerts do not count toward frequency caps
Admin Controls for Copy, Thresholds, Cadence, and Quiet Hours
Given an admin with Campaign Manager role opens Momentum Alerts settings When they configure proximity thresholds (1–5 donors), copy templates for proximity nudge and milestone burst, per-recipient cadence cap, and quiet hours window Then the form validates required placeholders (e.g., {{remaining_donors}}, {{milestone_label}}) and blocks save if missing or malformed And a preview renders the copy with sample data for the selected campaign And saving persists settings to the campaign and makes them effective within 5 minutes And an audit record is written with editor, changes, and timestamp And only authorized roles can view or edit these settings
Consent, Unsubscribe, and Privacy Compliance
Given a contact lacks SMS opt-in When an alert targets SMS Then the contact is excluded from SMS delivery And if a contact replies STOP to an SMS, they are suppressed for SMS within 60 seconds and receive a confirmation message And email alerts include a one-click unsubscribe that immediately updates suppression and is honored for future sends And push notifications honor device-level opt-out flags And each message includes required sender identification fields per channel configuration And consent timestamps and source are stored and exportable for audit upon request And suppression lists are enforced across all segments and channels
Admin Milestone Config
"As part-time staff without IT support, I want an easy way to set and preview milestones so that I can launch HeatBurst quickly and safely."
Description

Offer a simple, mobile-friendly configuration panel for staff to choose milestone types (donors, dollars, shifts), define markers (percentages and fixed amounts), set time windows, and select celebration/receipt templates. Include preview and test modes that simulate upcoming milestones with sample data before publishing. Enforce sensible defaults to reduce setup time, with role-based access controls and audit logs of changes for accountability.

Acceptance Criteria
Mobile Milestone Type Selection
Given a user with Milestone:Edit permission on a mobile device When they open the Admin Milestone Config panel Then they can select one or more milestone types: Donors, Dollars, Shifts Given no type is selected When the panel loads for first-time setup Then "Donors" is preselected by default Given types are selected When the user taps Save Then the selection persists and reloads within 1 second Given a selection is saved When the user switches to Preview Then only chosen types are included in upcoming milestones
Markers: Percentage and Fixed Amounts Validation
Given the admin chooses Percentage markers When they enter values Then each value must be an integer between 1 and 100 and unique Given the admin chooses Fixed Amount markers When they enter values Then each value must be a positive currency amount in the org’s currency and unique Given duplicate or out-of-range markers are entered When the user attempts to Save Then the Save is blocked and inline errors indicate the fields to correct Given valid markers are entered When the user taps Save Then markers are normalized (sorted ascending, currency formatted) and persisted Given mixed marker types are configured across milestone types When saved Then each type retains its own marker list without cross-contamination
Time Window Configuration
Given an admin opens Time Window settings When they set Start and End date/time Then Start must be before End and both use the org's timezone Given the admin opts for a rolling window When they select a duration (e.g., last 24h, 7d, 30d) Then the window is applied relative to now and reflected in Preview Given missing or invalid times When Save is tapped Then Save is blocked with accessible error messages Given a valid time window is saved When Preview is opened Then milestone calculations use only events within the window
Template Selection for Celebration and Receipt
Given available celebration and receipt templates exist When the admin opens Template settings Then the system lists templates with name, last updated, and thumbnail/summary Given the admin selects templates When they Save Then the association is persisted and reflected in Preview/Test Given a required template is missing When the admin attempts to Publish Then Publish is blocked with a clear prompt to select templates Given a template uses placeholders When in Preview/Test Then placeholders render with sample data; unresolved placeholders are highlighted
Preview and Test Simulation
Given a saved configuration When Preview is opened Then the system shows at least the next three upcoming milestones per type based on sample data Given Test mode is started When the admin simulates an event that crosses a marker (e.g., a $25 donation) Then the meter crosses the marker in Preview and the corresponding celebration and receipt previews update instantly Given Test mode is active When simulations are performed Then no live records, receipts, or notifications are created, and audit logs mark actions as "Test" Given sample data parameters are adjusted When Preview is refreshed Then outputs update within 2 seconds
Sensible Defaults and Quick Setup
Given first-time configuration When the panel loads Then defaults are prefilled: type=Donors, markers=50%, 75%, 100%, time window=Last 30 days, default templates selected if available Given defaults are acceptable When the admin taps "Use Defaults" then "Publish" Then the configuration publishes in 5 taps or fewer Given defaults are applied When Preview is opened Then upcoming milestones appear without further input
Role-Based Access Control and Audit Logging
Given user roles When accessing Admin Milestone Config Then users with Milestone:Edit can edit and publish; users without see read-only; unauthorized users are denied with 403 Given any create/update/delete of configuration When Save or Publish is performed Then an audit log entry is recorded with user, timestamp, changed fields before/after, and reason (if provided) Given audit logs exist When an admin filters by date range or user Then matching entries are returned within 2 seconds and exportable in CSV Given Test mode actions When logged Then entries are labeled "Test" and excluded from production change reports
HeatBurst Analytics & A/B Testing
"As a program lead, I want clear analytics on HeatBurst’s impact so that I can optimize settings and justify the feature to stakeholders."
Description

Measure the effect of HeatBurst on conversions, average gift, volunteer show-up, and time-to-second gift by logging exposures, triggers, and outcomes. Provide dashboards and exports that attribute lifts to milestones crossed, with cohort filters and campaign comparisons. Support A/B testing of milestone copy and celebration variants under feature flags, and feed summarized results into the Impact Board for real-time reporting.

Acceptance Criteria
Log HeatBurst Exposure and Trigger Events
- Given a HeatBurst-enabled screen is viewed for ≥1s, when the widget first renders in the viewport, then an exposure event is recorded within 500 ms with event_id, ISO 8601 UTC timestamp, user_id or anonymous_id, session_id, campaign_id, milestone_id (nullable), variant_id (nullable), page/screen, device_type, app_version, and geo (country). - Given a milestone bar crosses a threshold (e.g., 50%, 75%, 100%), when the celebration fires, then a trigger event is recorded exactly-once per trigger_id with pre_value, post_value, milestone_id, trigger_reason, celebrant_user_id (nullable), and idempotency_key; duplicates are deduped server-side. - Given a donation or shift signup completes within 30 minutes of an exposure, when the transaction is confirmed, then an outcome event is recorded with outcome_type, amount (minor units) and currency (if donation), and linked via attribution_id to the last exposure in that session. - When the device is offline, events are queued locally up to 1,000 entries and flushed within 5 minutes of reconnect, preserving timestamp order; server-side deduplication prevents double counts. - Data quality SLOs: ≥99% of events arrive within 10 minutes; <0.5% of events missing required fields; alert when 5‑minute error rate >1%.
Attribution Model for Milestone Crossings
- Default attribution window is 30 minutes post-exposure, configurable per campaign between 5–120 minutes; window setting is displayed on the dashboard. - Last-touch rule: the most recent HeatBurst exposure before the outcome receives credit; control exposures are valid attributions for control users. - Lifts are computed versus control for conversion rate, average gift, volunteer show-up rate, and median time-to-second gift; displayed as absolute and percent change. - Milestone-level attribution: lifts are also broken out by milestone thresholds crossed during the session (e.g., 50%, 75%, 100%) with a toggle to include/exclude sessions with no crossing. - Sanity guard: outcomes occurring before first exposure in a session are excluded from attribution. - Validation: on a seeded test dataset, computed conversion lift matches expected value within ±0.1 percentage points and average gift within ±$0.05.
Cohort Filters and Campaign Comparisons
- Dashboard provides filters: date range, campaign (multi-select), channel (web/app/link), device type, geography (country/state), donor status (new/returning), volunteer status (new/returning), and variant; active filters are clearly shown as chips. - Applying any single filter updates all widgets within 1.5s p95 for ≤1M events and 4s p95 for ≤10M events; loading indicators displayed during refresh. - Comparison view supports side-by-side metrics for up to 3 campaigns with normalization per 1,000 exposures; totals reconcile to filtered counts within ±1%. - Cohort definitions are documented via tooltip; clicking a cohort reveals counts used in denominators for each metric. - Exports and API responses reflect the exact set of active filters and comparison selections.
A/B Test Assignment and Consistency Under Feature Flags
- Users are randomized at user_id if known else anonymous_id; assignment is sticky for 90 days or until campaign end; re-install does not change assignment if anonymous_id persists. - Feature flags define variants (e.g., copy_a/copy_b, celebration_a/b) with adjustable weights (0–100%); weight changes propagate globally within 2 minutes. - Sample ratio mismatch is flagged when observed assignment deviates by >3 percentage points from target after ≥1,000 assignments; an alert event is emitted. - Control behavior: control users are shown no HeatBurst UI; exposures are still logged with variant_id = control. - A diagnostic endpoint returns the user’s current assignment for a given campaign without exposing PII beyond hashed identifiers.
Variant Performance Metrics and Significance
- Variant reports include per-variant: exposures, triggers, conversion rate, average gift (currency-aware), volunteer show-up rate, and median time-to-second gift; 95% confidence intervals are displayed with the statistical method noted. - Statistical significance is indicated when p < 0.05; significance badges are suppressed until each arm has ≥500 exposures or a configurable minimum. - Time-series view shows daily metrics with optional 7-day smoothing; missing days display as gaps (not zero-filled) unless explicitly toggled. - Exports include variant_id, metric values, sample sizes, CIs, and p-values; exported numbers match UI within rounding tolerance (±0.01 for rates, ±$0.01 for currency).
Real-time Impact Board Summaries
- Impact Board displays a HeatBurst tile with: donations attributed today and MTD, incremental lift vs control, milestones crossed today, and top-performing variant; auto-refreshes every 30 seconds with a visible last-updated timestamp. - Data freshness: p95 delay from event ingestion to Impact Board update is ≤60 seconds; when backlog delay exceeds 3 minutes, a “Delayed” badge appears. - Enablement is controlled per campaign via feature flag; the tile remains hidden until at least one experiment accrues ≥100 exposures. - On analytics outage, the tile shows the last successful snapshot not older than 2 hours along with a warning state; no stale data is presented as live.
Analytics Data Export and Schema Guarantees
- Admins can export raw events and aggregated metrics via UI and API in CSV and NDJSON; exports include documented columns: event_type, event_id, timestamp (UTC ISO 8601), user_id/anonymous_id, session_id, campaign_id, milestone_id, variant_id, outcome fields, and numeric metrics. - Exports respect all active filters and date ranges; maximum 5,000,000 rows per request; large exports stream with p95 start time <10 seconds. - Amounts are in minor currency units with currency code; PII fields are masked per org policy; timezone is always UTC. - Each export job returns row count and MD5 checksum; transient failures retry up to 3 times with exponential backoff.

TapToken Checkout

Enables truly one-tap add-ons for repeat donors using a previously consented, secure payment token, with mobile wallet fallback for first-timers. Cuts redirects and page loads, so $5 top-ups clear in seconds—even on spotty connections.

Requirements

Payment Token Consent & Vaulting
"As a repeat donor, I want to save my payment method as a secure token so that I can make future small add-ons with one tap without re-entering details."
Description

Implement secure capture of explicit donor consent and creation of a reusable payment token through the payment service provider, linking the token to the donor’s GiveCrew profile. Support network tokens and card-on-file via PCI-compliant vaulting, enforce token scoping to campaign/org, and provide token lifecycle management (create, rotate, suspend, revoke). Expose APIs to initiate charges with idempotency keys and store consent artifacts and timestamps for audit. Ensure encryption in transit and at rest, role-based access to token references (never raw PAN), and clear UX for opt-in/opt-out and deletion.

Acceptance Criteria
First-Time Consent Capture and Tokenization
- Given a first-time donor at checkout selects "Save payment method for TapToken" When the donor authorizes payment and any required SCA step Then the system records consent artifacts: donor_id, campaign_id, consent_text_version, consent_checked=true, timestamp (UTC ISO-8601), IP, user_agent/device_fingerprint hash, and an evidence hash or artifact link - Given payment succeeds and consent_checked=true When the PSP returns a token (network token or vaulted card reference) Then the system stores only token_ref and psp_customer_id linked to the donor profile and campaign/org scope; PAN/CVV are never stored - Given consent_checked=false When payment completes Then no tokenization occurs and a consent_declined event is logged; one-tap remains disabled - Given tokenization network and storage operations When data are transmitted or stored Then TLS 1.2+ is enforced in transit and token_ref storage is encrypted at rest; an audit log entry with actor, action, result, and PSP references is created
Repeat Donor One‑Tap Charge Using Scoped Token
- Given a donor has an active token scoped to Org=A and Campaign=C When the donor taps "Add $5" in the app Then the charge API is called with token_ref and an Idempotency-Key and returns a response p95 <= 2.0s on 3G; the donation is recorded, a receipt is sent within 30s, and the Impact Board updates within 10s - Given the same Idempotency-Key is reused within 24h When the client retries the request Then no duplicate PSP authorization occurs and the original charge_id and status are returned - Given the token scope does not match the requesting org/campaign When a one-tap charge is attempted Then the request is rejected with HTTP 403 and no PSP call is made - Given the PSP requires SCA for the tokenized charge When the step-up challenge completes Then the charge is resumed and completed without redirecting off-app; on failure, a wallet checkout fallback is offered - Given the token is suspended or revoked When the donor taps "Add $5" Then no charge attempt is made and the UI prompts to update payment; an event is logged
Token Rotation on Card Update
- Given the PSP notifies of token lifecycle update or a soft decline indicating card reissue When the system attempts the next charge or receives a webhook Then a new token is requested and stored; the old token is marked rotated and cannot be used for future charges - Given a token is rotated When subsequent one-tap charges are initiated Then they succeed using the new token without requiring the donor to re-enter payment details; p95 auth time <= 2.5s - Given token rotation occurs When records are updated Then audit logs include prior_token_ref, new_token_ref, actor/system, timestamps, and PSP references; the donor is notified only if re-consent is required
Suspension and Revocation by Donor or Admin
- Given a donor toggles off TapToken or requests deletion When the action is confirmed Then the token is revoked at the PSP when supported or marked revoked locally; state=revoked, one-tap disabled, and a confirmation message is sent within 1 minute - Given an org admin with payment_manage permission suspends a token When the suspension is applied Then state=suspended; one-tap is disabled; the admin may unsuspend to restore state=active - Given a suspended or revoked token When any charge is attempted Then the API returns HTTP 409 with code TOKEN_UNAVAILABLE and no PSP call is made; attempts are audit logged - Given a revocation request When processed Then propagation to the PSP completes p95 <= 10s; failures are retried up to 3 times with exponential backoff
Role-Based Access to Token References
- Given a user with Volunteer role views a donor When token data are requested Then only a boolean flag one_tap_enabled is returned; token_ref and payment metadata are omitted - Given a user with Staff role and payment_manage permission queries token details When the API responds Then it returns token_ref, brand, last4, exp_month, exp_year, and scope; PAN/CVV are never present; last4 and brand are masked appropriately - Given any API or export endpoint When responses are generated Then token_ref fields are encrypted at rest, all traffic uses TLS 1.2+, and access is logged with user_id, endpoint, timestamp, and result - Given an unauthorized user attempts to access token endpoints When the request is made Then the API returns HTTP 403 and no sensitive fields are emitted
Consent Artifact Retrieval and Audit Trail
- Given an auditor requests a donor’s consent record When an admin provides donor_id and campaign_id Then the system returns within 2s the consent artifacts: consent_text_version, locale, consent_timestamp (UTC), IP, user_agent/device hash, consent_channel, actor, and evidence hash or artifact link - Given multiple consents over time When records are retrieved Then they are versioned and immutable; updates create a new record preserving prior versions - Given regulatory retention requirements When data retention is evaluated Then consent artifacts are retained for at least 7 years unless a lawful deletion request is approved and logged with approver_id and reason - Given system clock configuration When timestamps are written Then they are NTP-synchronized with drift < 100 ms and stored in ISO-8601; automated tests verify monotonicity
One-Tap Add-on UI
"As a mobile donor, I want a single button to confirm a small add-on so that my donation clears instantly without extra screens."
Description

Create a single-tap checkout interaction that initiates an immediate charge against a previously saved token for preset micro-donation amounts (e.g., $3, $5, $10) without redirects. Provide configurable quick amounts, haptic/visual confirmation, and inline error recovery if a charge fails. Ensure accessibility (WCAG AA), large touch targets, and localization for currency and copy. Log events for tap, attempt, success, failure, and reason codes to analytics. Respect donor communication preferences and show clear amount and destination before tap.

Acceptance Criteria
Single-Tap Charge with Saved Token
Given a donor has an active saved payment token and at least one quick amount is configured And the One-Tap Add-on UI is visible When the donor taps the primary One-Tap button for the selected amount Then the charge is submitted immediately against the saved token without redirects or additional confirmation dialogs And the view remains in-place (no new pages/webviews) while showing a processing state within 200 ms of tap And haptic feedback and a visual progress indicator trigger on tap And a success state shows the charged amount and destination within 6 seconds at p95 on a simulated 3G profile
Configurable Quick Amounts
Given the organization has configured 3–5 quick add-on amounts When the One-Tap Add-on UI renders Then the configured amounts display as selectable chips with locale-aware currency formatting And the first configured amount is selected by default unless a campaign override is set And amounts below $1.00 or above $50.00 are not renderable/selectable And a configuration change appears to end users without a code deploy on next refresh And selection state is clearly indicated and programmatically determinable
Inline Error Recovery on Charge Failure
Given a charge attempt fails due to decline, network error, or token invalidation When the failure is received Then an inline error message appears below the One-Tap component with a human-readable reason mapped from the gateway code And a Retry action is available that reuses the same amount and idempotency key to prevent duplicate charges And if the token is invalid or expired, a Mobile Wallet fallback option is presented without leaving the screen And the UI preserves the user’s selection and focus, and allows canceling the attempt And a new attempt is only sent once per user action (no auto-retry loop)
Accessibility and Touch Target Compliance
Given the One-Tap Add-on UI is rendered on a mobile device Then all actionable controls have a minimum touch target of 44x44 CSS pixels And color contrast meets WCAG 2.2 AA (text >= 4.5:1, non-text UI >= 3:1) And all interactive elements expose accessible names/roles and follow a logical focus order And state changes (processing, success, error) are announced via ARIA live regions without stealing focus And haptic feedback respects the OS reduce/disable haptics setting
Localization of Currency and Copy
Given the app has a detected or selected locale and currency When rendering amounts and text Then amounts are formatted per ISO 4217 minor units and locale-specific separators and symbol placement And right-to-left languages render correctly with mirrored layout and proper text direction And all user-facing strings are sourced from localization files with fallback to English if missing And the same localized amount and destination appear consistently in the button, confirmation, and receipt messages
Analytics Event Logging with Reason Codes
Given a user interacts with the One-Tap Add-on UI When a tap occurs, an attempt is initiated, a success completes, or a failure occurs Then analytics events are emitted for tap, attempt, success, and failure using a shared correlation_id And each event includes amount, currency, destination_id, hashed donor_id, masked token_id, latency_ms, network_type (if available), and reason_code on failure And events are queued offline and flushed within 60 seconds after connectivity is restored And no PAN or full token values are logged; identifiers are masked per policy And event delivery success rate is at least 99% over a 24-hour window
Consent, Preferences, and Pre-Tap Disclosure
Given donor communication preferences (email, SMS, marketing) are stored When the One-Tap Add-on UI is shown Then the exact amount and destination are displayed clearly adjacent to the tap control And copy informs that tapping will immediately charge the selected amount And receipt delivery honors preferences (e.g., no SMS receipt if SMS is opted out) And no marketing follow-ups are triggered if the donor has opted out of marketing communications
Mobile Wallet Fallback & Tokenization Prompt
"As a first-time donor, I want to pay with my mobile wallet and be offered a quick way to save my details so that future donations are effortless."
Description

Detect absence of a saved token and automatically present Apple Pay/Google Pay as the primary checkout path for first-time donors. On successful wallet payment, prompt the donor to consent to store a reusable token for future one-tap add-ons, minimizing friction. Handle device/browser capability checks, country availability, and alternative card form fallback where wallets are unsupported. Maintain a consistent UX so that both wallet and tokenized flows converge on the same confirmation and receipt patterns.

Acceptance Criteria
First-Time Donor Wallet Fallback Presentation
Given a donor initiates checkout without a saved payment token And the device/browser supports Apple Pay or Google Pay And the donor’s country supports the detected wallet network When the checkout screen loads Then the supported wallet button is rendered as the primary action and is enabled And the card form is available as a secondary option And the wallet readiness check completes within 500 ms And the wallet button displays the correct brand and donation amount And no external redirect occurs prior to wallet authorization
Post-Wallet Payment Tokenization Consent
Given a first-time donor completes a successful wallet payment When the payment confirmation event is received by the app Then a single-screen prompt requests consent to store a reusable payment token And the prompt text includes purpose of use, revocation info, and a link to the privacy policy And if the donor selects Agree, a reusable token is created via the PSP and stored against the donor profile with network and last4 metadata And if the donor selects No thanks, no token is created or stored And in either selection, the donor proceeds without additional steps to the next screen
Wallet Unsupported Fallback to Card Form
Given a donor initiates checkout without a saved token And the device/browser or country does not support Apple Pay or Google Pay When the checkout screen loads Then the card form is presented as the primary payment method And no wallet button is shown And a successful card payment completes the donation and proceeds to confirmation And no tokenization consent prompt is shown after card payment
Wallet Payment Failure Retry and Method Switch
Given a donor attempts a wallet payment And the payment is declined or a network error occurs When the error is received Then an inline, actionable error message is displayed within 2 seconds And the donor can retry the wallet payment or switch to the card form without losing entered context And retries use an idempotency key to prevent duplicate charges And if the donor switches methods, the prior wallet attempt is not captured or settled
Unified Confirmation and Receipt Across Flows
Given a donation succeeds via wallet (with or without token consent) or via card fallback When the success event is received Then the app navigates to a standard confirmation screen showing donor name (if provided), amount, donation ID, and next steps CTA And the same receipt template (email/SMS) is sent within 2 minutes of success And the Impact Board metrics update within 60 seconds of success And the confirmation URL structure and tracking parameters are identical across methods
Consent Logging and Future Use Controls
Given the donor is shown the tokenization consent prompt after a wallet payment When the donor records a choice Then the system logs donor ID, timestamp, consent decision, payment network, device type, and consent text version And this record is retrievable via admin audit within 1 second And future one-tap add-ons can only use the token if consent = Agree And if consent is revoked from the donor profile, the token is disabled within 5 minutes and is no longer usable
Spotty-Connection Resilience & Retry
"As a field organizer using weak connectivity, I want charges to queue and complete reliably so that donors aren’t blocked and payments don’t double-charge."
Description

Optimize the checkout flow for unreliable mobile networks by using lightweight payloads, short timeouts, and automatic background retries with exponential backoff and idempotency keys. Provide optimistic UI feedback, queue a pending charge when offline, and reconcile once connectivity returns. Surface clear states for pending, confirmed, and failed transactions with options to cancel or retry. Ensure duplicate-charge protection across client and server and persist minimal local state securely until resolution.

Acceptance Criteria
Lightweight Requests & Short Timeouts
Given a repeat donor with a valid TapToken on a spotty connection When the donor initiates a $5 top-up Then the initial charge request payload size is <= 5 KB And the connection timeout per attempt is <= 3 seconds And the read timeout per attempt is <= 3 seconds And if no definitive result is received within 8 seconds, the attempt moves to background retry without blocking the UI
Exponential Backoff Retries with Jitter and Limits
Given a transient error occurs (network error, 408, 429, or 5xx) When retrying the same payment intent Then retries are scheduled at 2s, 4s, 8s, and 16s with ±20% jitter And a maximum of 5 total attempts (1 initial + 4 retries) is enforced And retries pause while the device is offline and resume on connectivity restoration And retries stop immediately on non-retriable responses (4xx other than 408/429)
Cross-Tier Idempotency Prevents Duplicate Charges
Given a payment intent is created with a unique idempotency key persisted locally When the user taps multiple times rapidly or the app resubmits after a timeout Then all submissions reuse the same idempotency key And the server returns the same result for the key for at least 24 hours And exactly one authorization/charge is created with the processor And the UI displays only a single receipt/confirmation
Offline Queueing and Auto-Reconciliation
Given the device is offline when the donor taps to confirm When the app detects no connectivity Then the payment intent is queued locally with status 'Pending' and visible in the UI And the user can navigate away without losing the queued intent And upon connectivity restoration, the app dispatches the queued intent within 5 seconds using FIFO order And the UI updates to 'Confirmed' or 'Failed' based on the server response
Optimistic UI with Clear States and Transitions
Given the user taps the checkout confirm button When the request is initiated Then a 'Processing' state is shown within 300 ms with a visible progress indicator And 'Pending' is shown when offline or awaiting retry And 'Confirmed' is shown on success with amount and timestamp And 'Failed' is shown on definitive failure with a human-readable reason And only valid transitions occur: Processing→Pending/Confirmed/Failed, Pending→Confirmed/Failed
User Cancel and Retry Controls
Given a transaction is 'Pending' and not yet dispatched When the user taps 'Cancel' Then the intent is removed from the local queue within 1 second and will not be dispatched And the UI confirms cancellation Given a transaction has 'Failed' due to a transient error When the user taps 'Retry' Then the app reuses the original idempotency key and resumes the backoff schedule And the UI returns to 'Processing' within 300 ms And cancel is disabled once the intent is dispatched or confirmed
Secure Minimal Local Persistence & Cleanup
Given any locally persisted payment intent When data is written to device storage Then only intentId, amount, currency, idempotencyKey, tokenReference, timestamps, and status are stored And data is encrypted at rest using the platform secure storage (Android Keystore/iOS Keychain) And upon resolution (Confirmed or non-retriable Failed), the local record is deleted within 5 minutes And on app restart, unresolved intents are restored for reconciliation without storing any sensitive payment data in plaintext
Fraud, Limits, and Compliance Controls
"As a finance admin, I want guardrails on one-tap charges so that we prevent fraud and stay compliant without adding friction for legitimate donors."
Description

Enforce per-donor and per-device velocity limits, daily/monthly caps, and risk checks specific to micro-donations. Support SCA/3DS where required, applying exemptions intelligently for low-value transactions while providing a step-up path if the issuer demands authentication. Maintain PCI-DSS compliance boundaries, log immutable consent and charge records, and implement BIN/country allow/deny rules as needed. Provide configurable thresholds in admin settings and integrate with PSP risk signals to minimize false declines.

Acceptance Criteria
Per-Donor and Per-Device Velocity Throttling for Micro-Donations
Given admin thresholds are set to per-donor 3 transactions per 5 minutes and per-device 5 transactions per 10 minutes When the same donor attempts a 4th TapToken charge within 5 minutes Then the transaction is declined before PSP authorization with reason_code="velocity_limit" and HTTP 409, a user-safe message is shown, and an audit log entry is written with donor_id, device_id, window_start, next_allowed_at And the cooldown resets after 5 minutes, allowing the donor to complete a new transaction successfully When a different donor uses the same device and the device has reached its per-device limit within 10 minutes Then the transaction is declined with reason_code="velocity_limit_device" and no PSP call is made And analytics counters for rate_limited_donor and rate_limited_device increment by 1 each time
Daily and Monthly Donation Caps Enforcement
Given caps are configured to per-donor daily USD 50 and monthly USD 200, and organization time zone is set to America/Chicago When a donor’s attempted TapToken charge would cause the daily total to exceed USD 50 Then the transaction is declined pre-authorization with reason_code="cap_exceeded_daily" and the response includes remaining_daily_amount=0 And no partial authorization is attempted and an immutable log entry is recorded When a donor’s attempted charge would cause the monthly total to exceed USD 200 Then the transaction is declined with reason_code="cap_exceeded_monthly" and remaining_monthly_amount=0 And at 00:00 local time, daily totals reset; on the 1st of the month at 00:00 local time, monthly totals reset
Intelligent SCA/3DS Exemptions with Risk Signal Integration and Step-Up
Given a donor in an EEA country attempts a EUR 5.00 TapToken charge and PSP risk_score < configured_threshold and LVP/TRA exemption is available When the authorization is requested Then a 3DS2 frictionless flow is used (no challenge), the exemption_type is recorded, and the payment is approved if issuer accepts the exemption When the issuer responds with challenge_required Then a 3DS2 challenge is presented inline; upon successful authentication the payment is captured, and upon failure or timeout it is declined with reason_code="authentication_failed" When the PSP risk_score >= configured_threshold or the card/country mandates SCA Then the system performs 3DS step-up regardless of low amount When the donor is outside SCA scope (e.g., non-EEA card and merchant) Then no 3DS is attempted and the decision and rationale are logged And all exemption/3DS decisions are logged with fields: exemption_type, risk_score, acs_trans_id (if present), outcome, issuer_result_code
BIN and Country Allow/Deny Enforcement
Given the BIN deny list includes prefixes [411111, 550000] and the country deny list includes [RU, KP], and the BIN allow list includes [400555] When a TapToken transaction uses a card with BIN 411111 Then the transaction is blocked before authorization with reason_code="bin_denied" When the inferred issuing country is RU Then the transaction is blocked before authorization with reason_code="country_denied" When the card BIN matches an explicit allow entry (400555) and the country is otherwise denied Then the allow list overrides the deny and the transaction proceeds to risk checks And updates to allow/deny lists propagate to all checkout nodes within 2 minutes (p95), applying to sessions initiated after propagation, and changes are captured in the admin audit log
Immutable Consent and Charge Audit Log
Given a donor grants TapToken consent When consent is captured Then an append-only record is written with fields: donor_id, token_ref, consent_text_hash, consent_at_utc, device_id_hash, ip, and record_hash linking to prev_hash When any TapToken authorization attempt (approved or declined) occurs Then a record is appended with psp_payment_id, amount, currency, outcome, reason_code, created_at_utc, and record_hash And the audit retrieval endpoint returns records in order and verifies hash-chain integrity, failing the request with 422 if tampering is detected And admin users cannot edit or delete records; attempts are denied with HTTP 403 and are themselves audited
PCI-DSS Boundary and Tokenization Compliance
Given TapToken Checkout is enabled When rendering payment entry for first-time donors Then PAN/CVV fields are provided only via PSP-hosted fields or mobile wallets and no GiveCrew domain receives PAN/CVV And a network inspection during checkout shows no PAN/CVV transmitted to GiveCrew services And a database/log scan using PAN-pattern detection finds zero PAN/CVV occurrences When processing repeat donations Then only PSP-issued tokens or wallet payment_method_ids are used; no raw card data is stored or processed by GiveCrew And the compliance checklist evidences SAQ A eligibility and current PSP AOC on file
Admin Thresholds Configuration and Propagation
Given an admin updates Fraud & Limits settings (per-donor velocity/window, per-device velocity/window, daily cap, monthly cap, risk_score thresholds) with valid values When the admin saves the configuration Then input is validated (ranges, units, currency), a new config_version is created, and the change is recorded in the audit log with who/when/what And the new configuration propagates to all enforcement services within 2 minutes (p95) and is reported healthy by a readiness endpoint When a TapToken checkout starts after propagation Then the new thresholds are enforced; sessions started before propagation continue using the previous config_version And the admin can rollback to the prior config_version in one action, with propagation and audit recorded
Receipts, Notifications, and Impact Board Updates
"As a donor, I want an immediate receipt and to see my contribution reflected on the Impact Board so that I feel confident and recognized."
Description

Auto-generate and send itemized receipts for one-tap charges via email/SMS according to donor preferences, with clear descriptors and refund instructions. Emit internal events/webhooks on authorization and capture to update donor history, campaign totals, and the live Impact Board in near real time. Deduplicate events to avoid double-counting, and include metadata (amount, campaign, token id reference, organizer) for reporting. Respect quiet hours and opt-outs for notifications.

Acceptance Criteria
Itemized Receipt Generation and Delivery
Given a successful one-tap capture using TapToken And the donor has a stored notification preference for email or SMS When the charge is captured Then the system generates an itemized receipt including: amount, currency, campaign name/id, organizer name/id, transaction id, token id reference, timestamp (UTC), charge descriptor, and refund instructions link/text And the email/SMS includes a clear charge descriptor and support contact And the receipt is queued if current time falls within the donor’s configured quiet hours (based on donor local timezone), otherwise sent immediately And when queued, it is delivered within 2 minutes after quiet hours end; when not queued, it is delivered within 2 minutes of capture And the delivery status (sent, queued, failed) is recorded on the donor’s timeline
Authorization and Capture Event Emission
Given a one-tap authorization or capture completes When the event is processed Then an internal event and an outbound webhook are published within 5 seconds of completion And the payload includes: event_type (authorization|capture), transaction_id, donor_id, organizer_id, campaign_id, amount, currency, token_id_reference, timestamp (UTC), charge_descriptor And the webhook is delivered via HTTPS, signed with a shared secret, and includes an Idempotency-Key header equal to transaction_id And a 2xx acknowledgment from a subscriber marks the delivery as complete; otherwise retries are scheduled per delivery policy
Impact Board Near Real-Time Update on Capture
Given a successful capture against a campaign When the capture event is emitted Then the live Impact Board reflects the new amount and counts within 5 seconds And only capture events (not authorizations) affect monetary totals And the update is attributed to the correct campaign and organizer And the board shows no duplicate increments for retried or duplicate events
Donor History and Campaign Totals Idempotency
Given duplicate authorization or capture events with the same Idempotency-Key/transaction_id arrive in any order When donor history and campaign totals are updated Then the system records exactly one donor history entry per unique transaction_id And campaign monetary totals increase exactly once per successful capture And authorization events do not increase monetary totals but update authorization status And processing is idempotent across retries and out-of-order delivery
Notification Quiet Hours and Opt-Out Enforcement
Given a donor has opted out of SMS or email notifications and/or has configured quiet hours in their local timezone When a one-tap charge is captured Then the system does not send notifications via any opted-out channel And if the preferred channel falls within quiet hours, the receipt is queued for delivery after quiet hours And queued receipts are delivered within 2 minutes after quiet hours end And an internal receipt record is stored with a reason code (opted_out|quiet_hours) and is not sent externally
Webhook Retry and Delivery Guarantees
Given a subscriber endpoint returns non-2xx or times out during webhook delivery When delivering authorization or capture webhooks Then the system retries delivery using exponential backoff for at least 24 hours And each retry reuses the same Idempotency-Key and transaction_id And downstream consumers experience at-most-once state changes due to idempotent processing on transaction_id And all delivery attempts and outcomes are logged and queryable by transaction_id
Admin Controls & Performance Reporting
"As an org admin, I want to configure one-tap settings and see performance metrics so that I can optimize conversions and keep the flow reliable."
Description

Provide an admin panel to enable/disable TapToken Checkout per org, configure default quick amounts, set limits, and manage messaging. Include dashboards for adoption, conversion, failure reasons, average processing time, retry success, and wallet-to-token opt-in rate. Offer CSV export and filters by campaign, organizer, and time range. Ensure role-based access control, audit logs for configuration changes, and SLIs/alerts for latency and error rates to maintain one-tap performance targets.

Acceptance Criteria
Org-Level Toggle, RBAC, and Audit Log for TapToken Checkout
Given an Org Admin with Payments:Configure permission When they toggle TapToken Checkout from On to Off Then one-tap options stop rendering for that org within 60 seconds and token charges are blocked And an immutable audit log entry is written with actor, timestamp (UTC), previous_value, new_value, org_id, ip, and optional reason Given a user without Payments:Configure permission When they attempt to change the TapToken toggle via UI or API Then the system returns 403 and no change is persisted And no new audit entry is created other than an access_denied attempt record Given a prior toggle change exists When an Org Admin restores the previous setting Then a new audit entry captures the revert with a reference to the prior change_id
Quick Amounts and Limits Configuration with Enforcement
Given an Org Admin configures quick amounts [$5,$10,$20], a per-transaction min $3, max $50, and a daily cap $100 per donor When a returning donor is shown one-tap options Then the UI renders exactly three buttons 5/10/20 and disables custom amounts outside [$3,$50] And any add-on that would exceed the $100 daily cap is blocked with a limit_reached message Given the admin attempts to save invalid quick amounts (duplicates, non-positive values, more than 5 options, or non-integer cents) When saving settings Then validation prevents save and inline errors enumerate each invalid field Given limits are active When a donor attempts a one-tap of $2 or $60 Then the transaction is not submitted and an explanatory error is displayed And the attempt is logged with reason under_limits or over_limits
Messaging Configuration with Preview, Versioning, and Controlled Publish
Given an Org Admin edits the one-tap headline, body, and CTA When they save as draft Then a new content version is created with version_id and not served to donors Given a draft exists When the admin taps Preview on mobile Then the preview renders the exact production template with sample data and device-specific styles Given a draft is ready When the admin publishes the version Then donors see the new messaging within 60 seconds And an audit entry records version_id_from and version_id_to Given a previously published version When the admin rolls back Then the prior version becomes active and an audit entry captures the rollback action
Performance & Adoption Dashboard with Filtered Metrics
Given campaign, organizer, and time-range filters are applied When the dashboard loads Then it displays metrics: adoption_rate, one_tap_conversion_rate, avg_processing_ms (mean), p95_processing_ms, retry_success_rate, wallet_to_token_opt_in_rate, and failure_reason distribution with definitions available via tooltip And all widgets recompute to match the applied filters within 3 seconds on a typical broadband connection Given a failure reason segment is selected When drilling down Then a detail view shows counts by device_type and a 7-day trend for the selected filters Given no data matches the filters When the dashboard loads Then zero-state messages are shown instead of errors
CSV Export Consistency with Dashboard Filters
Given campaign, organizer, and time-range filters are applied on the dashboard When the user triggers CSV export Then the exported file reflects the same filters and contains columns: date, campaign_id, organizer_id, taps, one_tap_conversions, adoption_rate, avg_processing_ms, p95_processing_ms, retry_success_rate, wallet_to_token_opt_in_rate, failure_reason, failure_count And the file is UTF-8 CSV with a header row and comma delimiter Given the org timezone is configured When exporting Then all date columns are rendered in the org timezone and include the timezone abbreviation in the header Given the same filters When comparing dashboard totals to CSV aggregates Then values match within +/-0.5% for rates and exactly for counts
SLIs and Alerts for One-Tap Latency and Error Rates
Given live production traffic When collecting telemetry Then SLIs are recorded every 60 seconds for p95 tap_to_confirm latency, payment_success_rate, tokenization_error_rate, and checkout_availability Given alert thresholds p95<=1500ms and payment_success_rate>=98.5% and tokenization_error_rate<1% When any 5-minute rolling window breaches a threshold Then a critical alert is sent to the On-call channel and email with runbook URL and current SLI values And duplicate alerts are deduplicated across channels until resolved Given a prior breach alert When metrics remain within thresholds for 10 consecutive minutes Then the alert auto-resolves and a recovery notification is sent
Role-Based Access to Settings, Dashboards, and Exports
Given a user with Org Admin role When accessing the TapToken admin panel Then they can view and edit all TapToken settings and view/export all dashboards across the org Given a user with Organizer role scoped to specific campaigns When accessing dashboards Then they can view metrics and export CSV only for their scoped campaigns and cannot modify org-level settings (controls disabled) Given a user with Viewer role When accessing the admin panel Then they can view dashboards but cannot export or change settings; export and save controls are hidden or disabled Given a permission change for a user When the user next performs an action Then the new permissions take effect immediately and the change is recorded in the audit log with actor, subject, and timestamp

Later Nudge

Offers quick-reply deferrals inside the receipt (e.g., “Later today,” “Tomorrow,” “This weekend”) and auto-schedules the same one-tap upsell at the chosen time. Honors quiet hours and stops asking after a decline, capturing intent without pressure.

Requirements

Receipt Quick-Reply Deferral UI
"As a supporter, I want quick “ask me later” options inside my receipt so that I can engage when it’s convenient without losing the opportunity."
Description

Embed quick-reply deferral options directly within the donation/shift receipt across supported channels (SMS, email, in-app). Present clear choices such as “Later today,” “Tomorrow,” and “This weekend,” plus a fallback “Pick a time” link for unsupported clients. For SMS, support both tappable links and keyword replies; for email, include action buttons with signed deep links; for in-app, render native buttons. Ensure accessibility, localization, and device responsiveness. Capture the selected deferral option with campaign/context identifiers and securely pass it to the scheduler. Fail gracefully by confirming selection and providing a “cancel” link. Log all selections for analytics and consent auditing.

Acceptance Criteria
SMS Receipt Deferral via Links and Keywords
Given an SMS receipt with visible deferral options and tappable links, When the recipient taps the "Later today" link, Then the system records a deferral_selected event with fields {receipt_id, recipient_id, campaign_id, channel="sms", option="later_today", context, timestamp} and Then a schedule_deferral request with a signed token containing the same fields is posted to the scheduler. Given the same SMS receipt, When the recipient replies with the keyword "TOMORROW", Then the system maps it to option="tomorrow", records the deferral_selected event, posts the schedule_deferral to the scheduler, and sends an acknowledgment SMS "Got it — we’ll check back tomorrow. Reply CANCEL to stop." within 5 seconds. Given an unsupported keyword is received, When processing the reply, Then the system returns an SMS "Sorry, try LATER, TOMORROW, WEEKEND, or CANCEL" and no scheduler call is made. Given any deferral link is activated more than once, When the second activation occurs, Then the system displays "This selection was already processed" and prevents duplicate schedule creation. Given a valid selection occurs during configured quiet hours, When the scheduler time is computed, Then the schedule is shifted to the next allowed window and the confirmation message reflects the adjusted time.
Email Receipt Deferral with Signed Action Buttons
Given an email receipt rendered in common clients, When the recipient clicks the "This weekend" action button, Then a deep link opens that includes a user-scoped, time-bound HMAC-SHA256 signature and Then the server validates the signature (TTL ≤ 10 minutes) before creating the schedule. Given the deep link signature is invalid or expired, When the link is opened, Then no schedule is created and an error page with retry guidance is shown (HTTP 401) and a security_event is logged. Given a valid click, When processing completes, Then a confirmation page displays the scheduled time and a "Cancel" link and Then the selection is logged with {receipt_id, recipient_id, campaign_id, channel="email", option}. Given images are blocked or buttons are not supported, When the email is viewed, Then a plain-text fallback link for each option is present and functions equivalently.
In-App Receipt Deferral via Native Buttons
Given a user views a donation/shift receipt in-app, When native buttons for "Later today", "Tomorrow", and "This weekend" are displayed, Then each button is focusable, has accessible labels, and is reachable via keyboard/assistive tech. Given the user taps any option, When the request is sent, Then a signed payload is posted to the scheduler and an in-app toast confirms selection within 2 seconds; a persistent "Cancel" action is available until execution time. Given the device is offline, When the user taps an option, Then the selection is queued with an idempotency key and retried within 60 seconds of reconnecting; the UI displays a queued state and prevents duplicate taps. Given the app receives a scheduler failure (non-2xx), When the response is handled, Then the UI shows a clear error with a retry option and no duplicate schedule is created.
Fallback "Pick a time" Flow for Unsupported Clients
Given the receipt is viewed in a client that does not support action buttons, When the user selects the "Pick a time" link, Then a mobile-friendly web page loads with a time picker localized to the recipient’s timezone and language. Given the user picks a custom time and confirms, When the form is submitted, Then the scheduler receives a signed payload {receipt_id, recipient_id, campaign_id, channel, selected_time_iso, context} and returns success, and the page displays a confirmation with a working "Cancel" link. Given JavaScript is disabled, When the page loads, Then the time selection and submission still function via a progressive enhancement fallback without breaking required validation. Given the user abandons the picker without confirming, When the session ends, Then no schedule is created and no analytics selection event is logged (view-only event is logged).
Accessibility, Localization, and Responsive Layout
Given the receipt UI is rendered across SMS previews, email, and in-app, When evaluated against WCAG 2.1 AA, Then tap targets are ≥ 44x44 px, color contrast ratios are ≥ 4.5:1, focus order is logical, and all buttons/links have programmatic names and roles. Given the organization has multiple locales configured, When the receipt is generated, Then option labels, confirmations, and cancel instructions are shown in the recipient’s preferred language with correct date/time formatting and RTL mirroring where applicable. Given the UI is displayed on a 320px-wide device, When options are rendered, Then there is no horizontal scrolling, text does not overflow, and options wrap gracefully on two lines without truncating meaning. Given dynamic text size is increased to 200%, When the UI is tested, Then the layout remains usable without loss of content or functionality.
Selection Logging and Consent Audit Trail
Given any deferral option is selected or canceled, When the event is processed, Then an immutable log entry is written with fields {event_type, receipt_id, recipient_id, campaign_id, option, channel, locale, timestamp, auth_subject, ip_hash, idempotency_key} and stored for ≥ 24 months. Given the same user repeats the same selection within 10 minutes, When the server processes the request, Then idempotency ensures only one schedule exists and duplicates are logged as deduplicated with no side effects. Given an auditor requests evidence, When exporting logs for a date range, Then the system can produce a CSV/JSON export within 60 seconds containing all relevant fields without exposing raw IP addresses (only hashed values). Given a logging failure occurs, When the scheduler call succeeds, Then the system retries log persistence up to 3 times with backoff and emits an operational alert if still failing.
Confirmation and Cancel Flow with Graceful Failure
Given a user makes a valid selection via any channel, When the action completes, Then the user receives a clear confirmation (SMS/email body or in-app toast/banner) that includes the scheduled time and a "Cancel" link or keyword. Given the user activates the cancel link or replies CANCEL, When the request is processed, Then the existing schedule is revoked, a cancellation confirmation is sent within 5 seconds, and a deferral_canceled event is logged. Given the scheduler returns an error or times out, When the user attempts a selection, Then the UI/message states "We couldn’t schedule that" with a retry path and provides contact or help without creating partial or duplicate schedules. Given the user’s client strips links (security filters), When confirmation is sent, Then clear plaintext cancel instructions are present (e.g., "Reply CANCEL" or a visible URL) that function without HTML/JS.
Nudge Scheduling Engine
"As an organizer, I want deferrals to automatically re-prompt at the chosen time so that follow-ups happen reliably without manual work."
Description

Implement a backend service that translates deferral selections into scheduled nudges for the same upsell. Resolve the supporter’s local timezone, handle daylight-saving changes, and compute the exact send window for options like “Later today,” “Tomorrow,” and “This weekend.” Ensure idempotency, deduplication, and safe retries with exponential backoff. Persist jobs in a durable queue with visibility timeouts and monitoring. Support channel-appropriate dispatch (SMS, email, push) and enforce per-org rate limits. Provide admin observability (metrics, logs, dead-letter queues) and an API for listing/canceling pending nudges.

Acceptance Criteria
Later Today Scheduling with Local Timezone and DST
Given org quiet hours configured 21:00–08:00 local and deferral policy "Later today" = selection_time + 3h (rounded to minute), When a supporter in America/Los_Angeles selects "Later today" at 2025-11-01 19:10 local, Then the nudge is scheduled for 2025-11-02 08:00 local (next allowed window) and the DST shift is correctly handled so the civil time is 08:00. Given the same settings, When a supporter in America/Los_Angeles selects "Later today" at 2025-03-09 13:25 local (DST start day), Then the nudge is scheduled for 2025-03-09 16:25 local and does not violate quiet hours. Given supporter timezone cannot be resolved, When a deferral is selected, Then the engine uses the org’s default timezone setting for scheduling and records timezone_source=org_default on the job.
Tomorrow and Weekend Window Computation
Given org deferral policies "Tomorrow" = next calendar day at 10:00 local and "This weekend" = upcoming Saturday at 10:00 local, and quiet hours 21:00–08:00, When a supporter selects "Tomorrow" on Tuesday at any time, Then the nudge is scheduled for Wednesday 10:00 local. Given the same policies, When a supporter selects "This weekend" on Thursday 14:00 local, Then the nudge is scheduled for Saturday 10:00 local. Given the same policies, When a supporter selects "This weekend" on Saturday after 10:00 or any time on Sunday, Then the nudge is scheduled for next Saturday 10:00 local. Given DST transitions, When the computed civil time is 10:00, Then the engine schedules exactly at 10:00 local regardless of UTC offset changes.
Idempotent Scheduling and Deduplication
Given an idempotency_key tied to the upsell interaction, When identical deferral events are received multiple times within 24h, Then only one scheduled job exists and subsequent schedule attempts return the existing job_id. Given a pending job exists for (supporter_id, upsell_id, scheduled_at), When another schedule request with the same tuple arrives, Then no duplicate job is enqueued due to a unique constraint on that tuple. Given a worker retry after a timeout, When the same job is reprocessed, Then downstream dispatch is protected by an idempotent send key so the nudge is sent at-most-once.
Queue Durability, Visibility Timeouts, and Safe Retries
Given jobs persist in a durable queue, When the scheduler process restarts mid-processing, Then no jobs are lost and in-flight jobs reappear after a 2-minute visibility timeout. Given a transient 5xx from the channel provider, When a dispatch attempt fails, Then the job is retried with exponential backoff starting at 30s, multiplier 2.0, jitter ±20%, up to 8 attempts, and metrics retries_total increments. Given a non-retryable error (e.g., 4xx InvalidRecipient), When dispatch fails, Then the job is moved to the dead-letter queue with reason_code and is not retried. Given a job exceeds max attempts, When the final attempt fails, Then the job transitions to DLQ and an alert is emitted within 60s.
Channel Dispatch, Quiet Hours, and Per-Org Rate Limits
Given supporter has SMS consent and phone on file and upsell channel=SMS, When the job becomes due, Then the SMS dispatcher is invoked and the message is not sent during quiet hours; if due time falls in quiet hours, it is rescheduled to the next quiet_hours_end window. Given org rate limit = 120 messages/minute, When due jobs would exceed the limit, Then the engine defers excess jobs so no minute exceeds 120 sends for that org and records rate_limit_deferrals metric. Given the selected channel is not deliverable (e.g., missing consent or address), When the job becomes due, Then the job is marked undeliverable with code NO_CHANNEL and is not retried.
Admin Observability and Nudge Management API
Given an authenticated org admin, When calling GET /v1/nudges?org_id={org}&supporter_id={sid}&status=pending&page_size=50, Then the API returns only that org’s pending nudges with id, scheduled_at, channel, attempts, and supports pagination and filtering by upsell_id. Given an authenticated org admin, When calling POST /v1/nudges/{id}/cancel, Then the job status becomes canceled and the nudge will not be dispatched even if previously due; the operation is idempotent. Given system metrics are scraped, When observed, Then per-org and global metrics are exposed: queue_depth, scheduled_lag_seconds, dispatch_latency_p50/p95, retry_rate, dlq_depth, rate_limit_deferrals; logs include correlation_id and idempotency_key for schedule and dispatch events.
Honor Decline and Opt-Out State
Given a supporter replies "No thanks" to the upsell thread, When the decline event is ingested, Then all pending nudges for that upsell/supporter are canceled within 60s and no further nudges for that upsell are scheduled until a new explicit opt-in is recorded. Given a supporter sends "STOP" via SMS, When consent is updated, Then all pending SMS nudges to that number are canceled, future SMS scheduling is rejected, and non-SMS channels remain unaffected unless separately opted out. Given duplicate decline/opt-out events, When processed, Then handling is idempotent and returns success without creating duplicates or errors.
Quiet Hours & Preference Enforcement
"As a supporter, I want follow-ups to respect my quiet hours so that I’m not contacted at inconvenient times."
Description

Honor organization-configured quiet hours and supporter-level do-not-disturb preferences for all scheduled nudges. If a computed send time falls within a restricted window or holiday, automatically shift to the next allowable window and update the job accordingly. Enforce per-channel rules (e.g., SMS time windows, TCPA/CTIA guidance) and respect opt-outs. Provide configuration UI and APIs to set quiet hours by org and program, and derive local send windows based on supporter timezone. Log enforcement decisions for auditability.

Acceptance Criteria
Shift Scheduled Nudges Outside Local Quiet Hours
Given an organization has quiet hours configured from 20:00 to 08:00 local time for push and SMS And a supporter’s timezone is America/Chicago And a Later Nudge follow-up is scheduled for 21:15 local time When the scheduler evaluates the job Then the send time is shifted to 08:00 on the next allowable day in the supporter’s local time And the queued job is updated in place (no duplicate job created) And an audit record is written with fields: rule=quiet_hours_shift, original_time, adjusted_time, timezone, channel, org_id, program_id, supporter_id, reason And messages exactly at 20:00 are treated as within quiet hours (shifted) and messages at 08:00 are allowed (not shifted)
Supporter Do Not Disturb and Opt-Out Suppression
Given a supporter has enabled Do Not Disturb for SMS from 18:00–09:00 local time When an SMS nudge is scheduled within that window Then the send is suppressed, the job status is set to suppressed_dnd, and no reschedule is created And an audit record is written with rule=dnd_suppression including the DND window and channel Given a supporter has opted out of SMS via STOP When any future SMS nudge is scheduled Then the send is permanently suppressed until a new opt-in is recorded, and the suppression list reflects the opt-out within 60 seconds And an audit record is written with rule=channel_opt_out including opt_out_timestamp and source Given a supporter declines a Later Nudge upsell When a follow-up for the same upsell category would be scheduled Then no follow-up is created and the decision is logged with rule=decline_preference
SMS Compliance: TCPA/CTIA Window and Opt-In Enforcement
Given SMS sends must occur between 08:00 and 21:00 in the supporter’s local timezone and require active opt-in When an SMS nudge is scheduled outside 08:00–21:00 local time Then the send time is shifted to the next occurrence of 08:00 local time on an allowable day Given a supporter lacks SMS opt-in or has a STOP status When an SMS nudge is evaluated Then the job is canceled (suppressed_opt_in_missing) and no future SMS nudges are sent until opt-in is restored And HELP and STOP keywords are processed such that HELP responses are returned within 10 seconds and STOP updates opt-out immediately And all compliance decisions are audit logged with rule=sms_compliance and include window_applied, opt_in_state, and keyword if applicable
Holiday and Program Blackout Rescheduling
Given an organization or program has configured blackout dates/holidays for a region And a supporter’s locale/region matches that holiday calendar When a nudge’s computed send time falls on a holiday or blackout date Then the send is shifted to the next non-blackout day at the start of the allowable window for the channel And if the blackout spans multiple consecutive days, the send is scheduled for the first permissible day after the blackout at window start And an audit record is written with rule=holiday_shift including holiday_id/name, region, original_time, and adjusted_time
Quiet Hours Configuration UI and API
Given an org admin with permissions opens Quiet Hours settings When they set default quiet hours per channel and add a program-level override Then values persist and effective resolution is program_override > org_default > system_default And validation prevents invalid ranges (start=end, overlapping ranges) and enforces 24h format And API endpoints (GET/PUT/PATCH) allow reading and updating quiet hours with schema validation and return 200 on success or 400 with error codes on invalid payloads And permission checks enforce that only authorized roles can read/write; unauthorized requests return 403 And all create/update/delete actions emit audit records with actor, before/after values, timestamp, and scope (org/program)
Timezone Resolution and DST Edge Cases
Given a supporter profile contains a timezone When scheduling a nudge Then the supporter’s timezone is used to compute allowable windows; if missing, the org default timezone is used Given a spring-forward DST transition where 02:00–02:59 local time does not exist When a send would fall in the missing interval Then it is shifted to the next valid time within the allowable window Given a fall-back DST transition where 01:00–01:59 repeats When a send is scheduled at a repeated time Then it is sent once at the first occurrence and no duplicate is created for the repeated wall-clock time And any timezone change on the supporter profile triggers re-evaluation of future jobs within 5 minutes and logs rule=timezone_change_reschedule
Audit Logging and Decision Traceability
Given any enforcement action occurs (quiet hours shift, DND suppression, holiday shift, opt-out, channel compliance) When the scheduler processes the job Then an immutable audit record is stored with fields: event_id, job_id, org_id, program_id, supporter_id, channel, original_time, adjusted_time (if any), rule_applied, reason_code, actor (system/user), timezone, locale/region, created_at And audit records are queryable by date range, org, program, supporter, channel, and rule via UI and API, and exportable as CSV And audit records are retained for at least 365 days And PII in audit views is limited to IDs by default, with expand-on-permission to view names/phones/emails
Decline Recognition & Suppression Logic
"As a supporter, I want the system to stop asking after I decline so that I don’t feel pressured."
Description

Detect and act on declines to prevent further nudges for the same upsell. Recognize explicit declines via a “No thanks” tap, reply keywords (e.g., NO), or equivalent negative intent. Immediately cancel pending jobs for that campaign/ask and set a configurable suppression interval while preserving eligibility for transactional communications. Store decline reason and context, update supporter state, and expose suppression status to organizers. Ensure STOP/UNSUBSCRIBE is still treated as global opt-out per compliance rules.

Acceptance Criteria
Explicit Decline via 'No thanks' Tap
Given a supporter views a receipt containing an upsell and taps "No thanks" When the event is received by the system Then any pending nudge jobs for the same campaign/ask are canceled within 60 seconds And a suppression record is created scoped to that campaign/ask with a default interval of 30 days (configurable) And the supporter remains eligible for transactional communications And the decline is stored with reason "explicit_tap", channel, campaign_id, ask_id, message_id, timestamp, and actor_id (supporter) And the supporter state is updated to reflect "suppressed_for_ask" And the suppression status is visible to organizers in the supporter profile and campaign roster And subsequent attempts to schedule the same upsell during the suppression interval are blocked And other campaigns/asks remain unaffected
Decline via Keyword Reply (e.g., NO)
Given a supporter replies "NO" to a nudge message for an upsell When the inbound message is processed Then the reply is recognized as a decline for that campaign/ask (case- and locale-insensitive, ignoring punctuation and whitespace) And "STOP" or "UNSUBSCRIBE" are treated as global opt-out and supersede ask-level suppression And any pending jobs for that campaign/ask are canceled within 60 seconds unless a global opt-out is applied, in which case all non-transactional sends are halted And a suppression record is created with reason "keyword_no" including normalized keyword and context fields And a confirmation is logged (without sending any additional nudge) that the preference was recorded And idempotency is ensured: repeated "NO" messages do not create duplicate records or errors
Negative Intent Detection (Free-text)
Given a supporter sends a free-text reply to a nudge message When the content matches a maintained list/model of negative-intent phrases with confidence >= 0.85 Then it is treated as a decline scoped to the campaign/ask And if confidence < 0.85 it is not treated as a decline and no suppression is applied And any pending jobs for that campaign/ask are canceled within 60 seconds upon decline classification And the stored decline record includes reason "nlp_negative_intent", confidence score, matched pattern, and context And evaluation on a labeled test set achieves precision >= 0.95 and recall >= 0.85 for negative intent classification
Suppression Interval Configuration and Expiry
Given organization-level default suppression is 30 days and a campaign overrides to 14 days When a supporter declines an upsell for that campaign Then the suppression expiration is set to 14 days from decline timestamp And after expiration, new nudges for the same campaign/ask may be scheduled And suppression applies only to the same campaign_id and ask_id; different asks are not blocked And editing the configuration updates future declines but does not retroactively change existing suppression records And the time zone of the supporter is used when calculating expiration boundaries
Quiet Hours and Deferral Job Cancellation After Decline
Given a supporter had previously selected a deferral option (e.g., "Tomorrow") for an upsell And quiet hours are configured for 9pm–8am in the supporter's time zone When the supporter declines before the deferral fires Then the scheduled deferral job for that campaign/ask is canceled immediately and not rescheduled And no further nudges for that campaign/ask are sent during the suppression interval regardless of quiet hours windows And transactional communications (receipts, confirmations) continue to be delivered outside and during quiet hours per policy
Organizer Visibility and API Exposure
Given an organizer views a supporter profile or campaign roster When the supporter has an active suppression for a campaign/ask Then the UI displays a suppression badge with reason, scope (campaign/ask), created_at, and expires_at And the suppression can be filtered in the roster view (e.g., show/hide suppressed) And an authenticated API endpoint GET /supporters/{id}/suppressions returns active and historical records with pagination And permissions ensure only organizers for the relevant campaign can view suppression details And audit logs capture who viewed suppression details and when
Compliance: Global Opt-out Handling
Given a supporter sends "STOP" or "UNSUBSCRIBE" in any channel When the inbound message is processed Then a global opt-out is applied immediately for all non-transactional communications across campaigns And ask-level suppression records are closed as superseded by global opt-out And a confirmation per channel policy is sent once, without additional upsell content And transactional communications remain eligible as allowed by compliance rules And the organizer UI and API reflect the global opt-out state distinctly from ask-level suppression
Context-Preserved Upsell Re-presenter
"As a supporter, I want the same ask to appear when nudged so that I can act in one tap without re-entering information."
Description

At nudge time, re-present the identical one-tap upsell from the original receipt with preserved context (campaign, shift, amount, payment method or shift slot). Validate that the ask is still valid (e.g., slot available, link unexpired); if not, provide a safe fallback (nearest shift, suggested amount) with clear messaging. Use signed deep links or tokens for frictionless one-tap acceptance, with secure handoff to the mobile workflow. Handle errors and show concise confirmations. Track conversions back to the originating receipt and deferral selection.

Acceptance Criteria
Re-present identical upsell with preserved context
Given a receipt contained a one-tap upsell with campaignId, upsellType (donation|shift), amount or slotId, paymentMethodRef, ctaLabel, and copyVersion When a Later Nudge triggers for that receipt Then the re-presented upsell uses the exact same campaignId, upsellType, amount or slotId, and paymentMethodRef And the UI copy and ctaLabel match the original copyVersion And no new options, amounts, or slots are introduced in the re-presented ask
Ask validity check and safe fallback with clear messaging
Given a re-presented upsell is prepared for sending When the original ask is invalid due to slot booked, token/link expired, campaign closed, or paymentMethodRef no longer available Then the system selects a fallback (nearest available shift in the same campaign within 14 days OR default suggested donation amount within campaign-defined range) And displays a concise message explaining the unavailability and the fallback offered And provides a single-tap CTA to accept the fallback And if no valid fallback exists, the upsell is omitted and a brief informative notice is displayed instead
Signed deep link one-tap acceptance with secure handoff
Given the user taps the one-tap CTA in the nudge Then a signed, single-use token containing receiptId, campaignId, upsellType, amount/slotId, paymentMethodRef, deferralChoice, and expiry is sent to the mobile workflow And the token expires no later than 24 hours after nudgeTime And the mobile workflow validates signature using rotating keys and rejects altered or expired tokens And on successful validation, the acceptance is executed without additional login and the user sees the confirmation screen And on validation failure, the user is routed to the authenticated path to complete or abandon And all network calls use HTTPS/TLS 1.2+
Quiet hours honored at re-present time
Given a nudge is scheduled for a specific local time And organization or user quiet hours are configured When the scheduled time falls within quiet hours Then delivery is deferred to the next permissible window in the user's timezone And no more than one deferral occurs per nudge And the actual send time and defer reason "quiet_hours" are logged
Error handling and concise confirmations
Given the user accepts the upsell When the operation succeeds Then a confirmation is shown within 2 seconds including amount/slot and date/time And a receipt or assignment confirmation is sent via the original channel within 60 seconds Given a network or processing error occurs after the user tapped accept When the retry option is used Then idempotency prevents duplicate charges or double-bookings And an error message under 140 characters is shown with a retry option and a link to contact support
Conversion and attribution tracking to originating receipt and deferral
Given any outcome occurs (accept, fallback accept, decline, no action) When the event is emitted Then it includes receiptId, deferralChoice, nudgeTime, scenarioId, tokenId, outcome, and eventTimestamp And conversions are attributed to the originating receipt and specific deferral choice And duplicate taps within 10 minutes are deduplicated via tokenId or idempotencyKey And analytics are visible on the Impact Board within 15 minutes of the event
Decline suppression for the same receipt and upsell
Given the user explicitly declines the re-presented upsell When subsequent nudge opportunities arise for the same receipt and upsell Then no further re-presentations are sent for that receipt/upsell And suppression only applies to that receipt/upsell pair and does not block unrelated campaigns And suppression expires after 90 days or when the campaign ends, whichever occurs first
Analytics & Impact Board Integration
"As an organizer, I want to see how Later Nudge affects conversions so that I can optimize outreach and staffing."
Description

Instrument events for deferral selections, scheduled sends, deliveries, declines, and conversions. Compute KPIs such as deferral rate, nudge delivery rate, conversion after nudge, and uplift versus control. Expose metrics in dashboards and feed summary tiles to the Impact Board (e.g., added signups, donations, and show-up lift attributable to Later Nudge). Support filtering by program, campaign, channel, and timeframe, and export CSV for deeper analysis. Ensure privacy by aggregating where necessary and honoring data retention policies.

Acceptance Criteria
Event Instrumentation for Later Nudge Lifecycle
Given a receipt containing deferral options is displayed When the receipt is delivered or viewed (channel-dependent) Then an event "deferral_prompt_shown" is recorded within 5 seconds with fields: user_id_hash, program_id, campaign_id, channel, nudge_id, event_timestamp_utc Given a recipient selects a deferral option in the receipt When the system processes the action Then an event "nudge_deferral_selected" is recorded within 5 seconds with fields: user_id_hash, program_id, campaign_id, channel, nudge_id, deferral_option, event_timestamp_utc Given an eligible recipient is evaluated for Later Nudge When cohort assignment occurs Then the cohort value in {"treatment","control"} is recorded as a property on all subsequent nudge events and is available as a filter dimension Given a deferred nudge is scheduled When the schedule is created or updated Then an event "nudge_scheduled" is recorded with fields: user_id_hash, nudge_id, scheduled_send_time_utc, timezone, program_id, campaign_id, channel Given a scheduled nudge attempts delivery When the provider reports success or definitive failure Then an event "nudge_delivered" or "nudge_delivery_failed" is recorded with fields: user_id_hash, nudge_id, provider_message_id, status, delivered_timestamp_utc, latency_ms Given the recipient explicitly declines the upsell When the decline input is received Then an event "nudge_declined" is recorded with fields: user_id_hash, nudge_id, reason, event_timestamp_utc Given the recipient completes the target conversion action associated to the nudge within the configured attribution window When the conversion is logged Then an event "nudge_converted" is recorded with fields: user_id_hash, nudge_id, conversion_type, conversion_value, conversion_timestamp_utc Given duplicate submissions occur due to retries When events are ingested Then duplicates are deduplicated using an idempotency key and only one record persists per logical event
KPI Computation: Deferral and Nudge Delivery Rates
Given events within a selected timeframe and applied filters (program, campaign, channel) When KPIs are computed Then Deferral Rate = count(deferral_prompt_shown with at least one eligible recipient) denominator and count(nudge_deferral_selected) numerator; Nudge Delivery Rate = count(nudge_delivered) / count(nudge_scheduled), rounded to 0.1%, and results match a hand-validated sample within 0.1% Given events with transient delivery statuses When computing Nudge Delivery Rate Then only final statuses (delivered or definitive failure) within the timeframe are included; in-flight messages are excluded from both numerator and denominator Given filters are applied When the KPI query runs Then numerators and denominators include only events matching all selected filters and the configured organization timezone is used for date boundaries
KPI Computation: Conversion After Nudge and Uplift vs Control
Given events labeled with cohort in {"treatment","control"} and a configured attribution window W When computing conversion metrics Then conversion_after_nudge_treatment = count(nudge_converted where cohort=treatment and within W) / count(nudge_delivered where cohort=treatment); conversion_after_nudge_control = count(conversions of the same type within W among eligible control) / count(eligible control), uplift_abs = treatment_rate - control_rate, uplift_rel = (treatment_rate - control_rate) / control_rate, all rounded to 0.1% Given the control sample size is below the configured minimum K When computing uplift Then uplift values are suppressed and the dashboard returns an "Insufficient data" state for uplift, while still showing treatment conversion rate Given validation datasets with known outcomes When the computation runs Then reported rates and uplift match the validation within 0.1% and totals reconcile with underlying event counts
Dashboard Metrics with Filter Controls
Given a user with Analytics access opens the Later Nudge dashboard When the default view loads Then KPI cards for Deferral Rate, Nudge Delivery Rate, Conversion After Nudge, and Uplift vs Control are displayed for the last 7 days with All programs/campaigns/channels and render within 2 seconds after API response Given the user selects one or more values for program, campaign, channel, and timeframe When filters are applied Then all visualizations and KPI cards update to reflect the filters, API responds within 1.5 seconds at p95, and client renders updated metrics without full page reload Given filters produce no matching data When the dashboard renders Then zero-value KPIs and "No data" visual states are shown without errors, and export is disabled until data is present Given the organization timezone is set When time-bucketed charts are displayed Then day and week boundaries use the organization timezone for aggregation
Impact Board Summary Tiles: Attributable Lift and Added Outcomes
Given Impact Board tiles are configured for Later Nudge (Added Signups, Added Donations Amount, Show-up Lift) When the daily rollup job runs at 02:00 in the organization timezone Then tiles update with values attributable to Later Nudge using uplift vs control for the current reporting period, and the values match the Analytics dashboard within currency rounding or 0.1% Given a user clicks an Impact Board tile When navigation occurs Then the user is taken to the Analytics dashboard pre-filtered to the corresponding program/campaign/channel and timeframe Given sample sizes below the configured minimum K When rendering tiles Then the tile value displays "—" with an "Insufficient data" tooltip and no personally identifiable information is shown
CSV Export for Filtered Metrics and Aggregates
Given filters (program, campaign, channel, timeframe) are applied on the dashboard When the user requests Export CSV Then a CSV is generated with a header and columns: date, program_id, program_name, campaign_id, campaign_name, channel, cohort, deferrals, deferral_rate, nudges_scheduled, nudges_delivered, delivery_rate, conversions, conversion_rate, uplift_abs, uplift_rel; values reflect the filters and timeframe; date is ISO 8601; numeric values use dot decimal; and file downloads within 10 seconds for datasets <= 100k rows Given an export would produce > 100k rows When the user requests Export CSV Then an asynchronous export is created and a downloadable link is delivered in-app or via email within 15 minutes at p95, and the export reflects the same filters used on the dashboard Given privacy and retention policies are enforced When exporting Then no raw message content or PII fields are included; user identifiers are hashed; and rows that violate the small-slice threshold K are suppressed or aggregated to an "Other" bucket
Privacy and Data Retention Compliance
Given a configured data_retention_days value R When current_date > event_timestamp + R days Then events are excluded from new computations and purged from the analytics store within 24 hours, and exports/dashboards no longer surface those events Given a configured privacy_threshold_k value K When aggregating metrics for any filter slice Then slices with unique_recipient_count < K are suppressed in dashboards and exports, while higher-level totals remain available without leaking small cell values Given sample exports and API responses When inspected Then no columns named email, phone, or name are present; user_id_hash values are 64-hex characters and differ from any raw identifiers observed elsewhere in the system

Share Sprint

After a top-up, includes a one-tap forward link with prefilled text and a personal mini-goal (e.g., “I’m rallying 5 friends to add $5”). The shared link shows a tiny progress meter for that mini-goal, turning donors into micro-ambassadors and multiplying small-dollar gifts fast.

Requirements

Personal Mini-Goal Creation & Link Tokenization
"As a donor, I want a personal mini-goal and shareable link created automatically after I give so that I can quickly rally friends without extra setup."
Description

On successful top-up, automatically create a personal mini-goal with sensible defaults (e.g., 5 friends at $5 each) that the donor can edit before sharing. Generate a unique, non-guessable, time-bound token and branded short link that ties donations back to the ambassador’s sprint and campaign. Persist goal parameters, start/end timestamps, status (active, completed, expired), and cumulative metrics (clicks, donors, dollars). Support concurrent sprints per user with deduping for rapid repeat top-ups. Provide create/read/update/close APIs and webhook events for milestones. Include currency and locale awareness, UTM channel parameters, and secure revocation/invalid link handling.

Acceptance Criteria
Auto Mini-Goal Creation on Successful Top-Up
- Given a donor’s top-up payment is confirmed as succeeded, When the top-up completion event is processed, Then exactly one mini-goal is created for that top-up with default target_friends and per_friend_amount based on configuration and donor currency. - Given the mini-goal is created, Then start_timestamp is set to creation time, end_timestamp is set to start_timestamp plus the configured default duration, and status is set to active. - Given duplicate receipt of the same top-up event id within 24 hours, When creation is attempted, Then no additional mini-goal is created and the existing record is returned. - Given the mini-goal exists, Then prefilled share text is generated including target_friends and per_friend_amount localized to the donor’s locale.
Mini-Goal Editing Before Sharing
- Given a default mini-goal exists, When the donor opens the Share Sprint editor, Then target_friends and per_friend_amount are pre-populated using localized number and currency formats. - Given the donor changes target_friends and/or per_friend_amount within configured bounds, When Save & Get Link is tapped, Then the mini-goal is updated and the prefilled share text is regenerated accordingly. - Given the donor enters invalid or out-of-bounds values, Then inline validation messages are shown and Save is disabled until corrected. - Given the donor cancels editing, Then no changes are persisted to the mini-goal.
Secure Tokenization and Branded Short Link
- Given a mini-goal is active, When generating a share link, Then a unique URL-safe token with at least 128 bits of entropy is created and not reused across sprints. - Then the short link uses the configured branded domain and resolves to the campaign’s share landing with the token and UTM parameters for source, medium, campaign, and content. - Then the token’s validity window is bounded by the mini-goal start/end timestamps and cannot be used after expiry. - Given token generation detects a uniqueness conflict, Then a new token is generated until unique and persisted atomically. - Given a donation is completed via the short link, Then the donation is attributed to the ambassador’s mini-goal and parent campaign.
Metrics Persistence and Status Lifecycle
- Then the system persists goal parameters (target_friends, per_friend_amount, currency, locale), start_timestamp, end_timestamp, and status. - Given link clicks, donor conversions, and dollars received via the tokenized link, When events occur, Then cumulative metrics clicks, donors, and dollars are incremented and available via the read API within 60 seconds. - Given cumulative donors >= target_friends OR cumulative dollars >= target_friends * per_friend_amount, Then status transitions to completed and a target_reached webhook is emitted. - Given current time > end_timestamp and status != completed, Then status transitions to expired and an expired webhook is emitted. - Given status is completed or expired, Then further donations via the token are not attributed to the sprint.
Concurrency and Dedupe for Rapid Repeat Top-Ups
- Given a user completes multiple distinct top-ups, When each top-up completion event is processed, Then a separate active mini-goal with a distinct token is created for each. - Given webhook retries or duplicate UI submissions for the same top-up event id within 5 minutes, Then no duplicate mini-goals are created for that event id. - Given multiple active mini-goals for a user, When a donation is made through one sprint’s link, Then attribution applies only to that sprint and not to others.
APIs and Webhooks with Locale and UTM Support
- Given authenticated requests, When POST /sprints is called with valid input or defaults, Then 201 is returned with sprint_id, tokenized short link, goal parameters, timestamps, status, and metrics initialized to zero. - Given an existing sprint, When GET /sprints/{id} is called, Then 200 is returned with persisted fields and up-to-date metrics; When GET /sprints?filters… is called, Then pagination and filtering by user, campaign, and status are supported. - Given an active sprint, When PATCH /sprints/{id} updates editable fields within constraints, Then 200 is returned and prefilled share text is regenerated; attempting to edit a completed/expired sprint returns 409. - Given an active sprint, When POST /sprints/{id}/close is called, Then status becomes completed, attribution stops, and a closed webhook is emitted. - Given webhook subscriptions, When sprint events occur (created, first_click, first_donor, target_reached, expired, revoked), Then signed HMAC webhooks are delivered with retries and include sprint_id, user_id, campaign_id, currency, locale, and metrics. - Given share channel selection (e.g., SMS, WhatsApp, email), Then UTM parameters reflect the channel and are recorded on downstream donations. - Given currency and locale settings, Then per_friend_amount is stored in minor units with currency code and rendered in the donor’s locale for all responses.
Revocation, Expiry, and Invalid Link Handling
- Given an admin or authorized user revokes a sprint, When the short link is visited, Then no attribution occurs, the user sees a not-active message, and the API returns 410 Gone for token resolution. - Given an invalid or malformed token, When it is used, Then the request is rejected, a generic campaign page may be offered without attribution, and a security event is logged. - Given a sprint expires or is revoked, Then the token is invalidated across CDN/cache within 5 minutes and cannot be used to start a donation flow. - Given revocation occurs, Then a revoked webhook is emitted and audit logs store who performed the revocation and when. - Given a revoked sprint is reinstated, Then a new token and short link are issued and the old token remains invalid.
Cross-Channel Prefilled Share Messaging
"As a donor, I want ready-to-send messages tailored to each channel so that sharing my mini-goal is fast and effective."
Description

Provide channel-specific, prefilled share messages for SMS, WhatsApp, email, social, and copy-to-clipboard, using personalization tokens (first name, org name, goal count/amount) and the sprint short link. Enforce per-channel constraints (character limits, link placement), render previews, and allow last-mile user edits. Support organization-managed templates with localization, simple variant selection, and branded short domains. Ensure compliant language and optional disclaimers where required by the organization’s comms policy.

Acceptance Criteria
Generate Prefilled Share Message Per Channel
Given an authenticated organizer completes a top-up and initiates Share Sprint When they select a share channel (SMS, WhatsApp, email, social, or copy-to-clipboard) Then the system generates a prefilled message for that channel with tokens {first_name}, {org_name}, {goal_count}, {goal_amount} resolved and the sprint short link included And the message meets the channel’s configured constraints (max_length, required_link_position, allowed_characters); if overflow would occur, the system trims per rules while preserving the link and any required disclaimer and displays the overflow count And for email, both subject and body are generated; for other channels, a single message body is generated And if any token value is missing, the system uses the organization’s configured fallback value and never renders the raw token literal
Render Channel-Specific Preview
Given a prefilled message is generated for a selected channel When the user opens the preview Then the preview displays exactly the final text that will be sent or copied for that channel, including link placement and any disclaimer And a live character counter shows current length versus the channel’s configured max_length (or “No limit” if none) And for email, the preview shows both subject and body in their respective fields And all tokens appear resolved with locale-appropriate formatting for numbers and currency
Allow Last-Mile Edits With Validation
Given a channel-specific preview is displayed When the user edits the prefilled text before sending or copying Then the system revalidates channel constraints in real time and shows pass/fail state and errors inline And the Send/Share/Copy action is disabled while constraints are violated and enabled when satisfied And the user can revert to the default template with a single action that restores the last generated text
Apply Localized Templates With Fallback
Given organization-managed templates exist for the selected channel in multiple locales When the user’s selected language/locale matches an available template Then that localized template is used to generate the message, including localized disclaimers When no localized template exists for the selected locale Then the system falls back to the organization’s configured default locale template
Select and Switch Template Variants
Given two or more simple variants (e.g., Friendly, Urgent) are configured for the selected channel When the user selects a variant Then the preview updates immediately to reflect that variant’s content with tokens resolved And the selected variant persists for the current Share Sprint session When the user has edited the message and attempts to switch variants Then the system prompts to keep edits (apply to new variant) or discard edits (reset to variant default) and proceeds according to the user’s choice
Use Branded Short Domain With Fallback
Given the organization has a branded short domain configured and healthy When the sprint short link is generated Then the link uses the organization’s branded short domain and resolves to the intended share URL When the branded domain is unavailable or exceeds quota Then the system automatically falls back to the platform default short domain and notifies the user non-blockingly in the preview And in all cases, the link length and placement satisfy the selected channel’s constraints
Enforce Comms Policy and Disclaimers
Given the organization’s comms policy defines prohibited phrases and required or optional disclaimers per channel and locale When a prefilled message is generated or edited Then required disclaimers are auto-included and cannot be removed And optional disclaimers can be toggled by the user And if prohibited content is detected, the system blocks sending/copying and shows a specific violation message listing the offending terms
Native Share Sheet & Web Share Integration
"As a mobile user, I want to share with one tap using my phone’s native share options so that I can spread the word with minimal friction."
Description

Enable one-tap sharing by invoking native iOS/Android share sheets in the mobile app and the Web Share API v2 on supported browsers, with graceful fallback to quick-channel buttons and copy-to-clipboard. Attach the prefilled text and sprint link, deep-linking to installed apps when possible. Record the selected channel for analytics and append UTM parameters accordingly. Handle offline mode with queued shares, permission denials, and retries. Meet accessibility standards for focus order and keyboard navigation.

Acceptance Criteria
One-Tap Native Share on iOS with Prefill
Given an iOS user viewing a Share Sprint prompt and connected to the internet When the user taps the Share button Then the iOS native share sheet opens within 500 ms And the share payload includes prefilled text with the user’s first name, mini-goal line, and the sprint link And the sprint link is a valid universal/app link that deep-links to the GiveCrew app when installed, else opens the web landing And upon completion/cancel, the app records an analytics event with outcome (success|cancel) and activityType when available And the sprint link includes UTM parameters: utm_source=<mapped activityType or "share_sheet">, utm_medium=share, utm_campaign=share_sprint, utm_content=<mini_goal_id> And no crashes or UI freezes occur during the flow
One-Tap Native Share on Android with Prefill
Given an Android user viewing a Share Sprint prompt and connected to the internet When the user taps the Share button Then the Android chooser opens within 500 ms And the share payload includes prefilled text with the user’s first name, mini-goal line, and the sprint link And if the user selects a target app, the selected package name is captured for analytics when available And the sprint link includes UTM parameters: utm_source=<mapped package or "share_sheet">, utm_medium=share, utm_campaign=share_sprint, utm_content=<mini_goal_id> And the sprint link deep-links to the GiveCrew app when installed, else opens the web landing And no crashes or ANRs occur during the flow
Web Share API v2 Invocation with Fallback
Given a web user on a browser that supports Web Share API v2 When the user taps the Share button Then navigator.share is invoked with title, text, and URL within 300 ms And on resolve, an analytics event is recorded with outcome=success and utm_source=web_share And on rejection or exception, an analytics event is recorded with outcome=cancel|error, and the UI falls back to quick-channel buttons and Copy Link within 200 ms Given a web user on a browser that does not support Web Share API v2 When the user taps the Share button Then the UI immediately shows quick-channel buttons (SMS, WhatsApp, Email, etc.) and Copy Link, all keyboard-focusable And the shared URL contains UTM parameters: utm_source=<selected channel or copy>, utm_medium=share, utm_campaign=share_sprint, utm_content=<mini_goal_id>
Quick-Channel Buttons and Deep-Link Handling
Given quick-channel buttons are displayed (e.g., SMS, WhatsApp, Email, Messenger) When a user selects a channel with a corresponding installed app Then the app opens the channel via deep link/intent within 500 ms with the prefilled text and sprint link present in the compose view When a user selects a channel without the app installed Then the flow opens the appropriate web endpoint or app store page with messaging explaining the fallback And in all cases, the sprint link includes UTM parameters mapping utm_source to the chosen channel (e.g., whatsapp, sms, email, messenger) And an analytics event records the selected_channel, sprint_id, user_id, and timestamp And Copy Link copies the full message (prefilled text + sprint link) to clipboard within 100 ms and shows a confirmation toast for 2–3 seconds
Offline Mode Queue and Retry Behavior
Given the device is offline when the user initiates share When the user taps Share or a quick-channel button Then the share action is queued with payload (text, link with UTM, intended channel) and a toast indicates it will send when online And no share UI that requires connectivity is opened while offline When connectivity is restored Then the app prompts the user to complete the share and re-opens the appropriate share UI within 5 seconds And the system retries automatically up to 3 times over 10 minutes if the user does not respond, after which the queue item is marked expired And if a permission denial occurs (e.g., clipboard blocked), the item is marked failed with a visible error and is not auto-retried And all queue state transitions are auditable in logs with timestamps
Channel Analytics and UTM Attribution
Given any share path (native share sheet, web share, quick-channel, copy) When the share is initiated Then an event ShareInitiated is recorded with properties: share_id (UUID), user_id, sprint_id, platform, surface, intended_channel, timestamp When the share is completed or canceled (where detectable) Then an event ShareResult is recorded with properties: share_id, outcome (success|cancel|error), selected_channel (or share_sheet), activityType/package where available And the sprint link includes UTM parameters: utm_source=<selected or intended channel>, utm_medium=share, utm_campaign=share_sprint, utm_content=<mini_goal_id> And landing pages receive and parse these UTM parameters, storing them alongside donation/registration events for attribution And 95% of analytics events are delivered within 2 seconds of occurrence with at-most-once semantics using share_id de-duplication
Accessibility and Keyboard Navigation Compliance
Given a user relying on keyboard or assistive technologies When navigating the Share UI Then focus order follows visual order, all actionable elements are reachable via Tab/Shift+Tab (web) or standard focus navigation (mobile), and Enter/Space activate controls And all icons/buttons have accessible labels and role semantics (ARIA on web), and screen readers announce the purpose and state of share actions and toasts And touch targets meet minimum 44x44 dp/pt, color contrast meets WCAG AA, and there are no focus traps And the share sheet invocation is announced via accessibility events, and errors/success toasts are announced politely
Mini-Goal Progress Meter Landing Page
"As a friend receiving the link, I want a clear progress page with an easy donation flow so that I can contribute in seconds and see the impact."
Description

Serve a fast, responsive sprint landing page that displays the ambassador’s name (or anonymous), organization branding, and a compact progress meter showing friends joined and dollars raised versus the mini-goal. Present a donation CTA with $5 default and mobile wallets (Apple Pay/Google Pay) where available, minimizing fields for frictionless completion. Update the meter in real time after each donation and transition states on completion or expiry with appropriate messaging. Support localization, currency formatting, Open Graph metadata for rich previews, and WCAG AA accessibility. Include basic fraud mitigation (rate limiting, bot checks) and safe fallbacks when tokens are invalid.

Acceptance Criteria
Landing Page Rendering, Branding, and Safe Fallback
Given a valid share token and a mobile viewport of 360x640 on simulated 4G, When the landing page loads cold cache, Then Largest Contentful Paint is ≤ 2.5s and no horizontal scrolling occurs. Given a valid share token mapped to an ambassador with a first name, When the page renders, Then the header shows the ambassador’s first name; and if no name is available, it displays “Anonymous”. Given organization branding assets exist, When the page renders, Then the org logo appears with descriptive alt text and theme colors are applied; and if branding assets are missing, default GiveCrew branding is applied without visual breakage. Given an invalid or expired share token, When the page is visited, Then a safe fallback page renders that omits the progress meter and ambassador name, displays generic campaign messaging, and offers a general Donate CTA not tied to a mini‑goal.
Progress Meter Accuracy and Real‑Time Updates
Given a mini‑goal of F friends and D dollars, When the page loads, Then the progress area displays Friends: x/F and Dollars: y/D and a meter that reflects progress as min(100%, floor(min(x/F, y/D)×100)). Given a successful new donor completes checkout, When the processor confirms payment, Then within 2 seconds the page updates without full reload, incrementing friends by 1 and dollars by the charged amount, and the meter updates accordingly. Given the page is refreshed after contributions, When it reloads, Then counts and meter match server truth with no double‑counting. Given duplicate payment webhooks or retries occur, When idempotency keys match, Then the meter and counts are not incremented more than once. Given over‑goal contributions, When x>F or y>D, Then the meter remains at 100% and an “over goal” badge displays with the overage amount or friend count.
Donation CTA and Mobile Wallet Checkout
Given the landing page loads on a device/browser with Apple Pay or Google Pay availability, When the donor taps the wallet button, Then a $5 default donation can be completed in ≤ 3 taps without manual entry, and success returns to the landing page. Given wallets are unavailable, When the donor taps Donate, Then a minimal card form (amount, name, email, card) is shown with $5 preselected and client‑side validation for required fields. Given the donor enters an amount < $1 or > $2,000, When submitting, Then submission is blocked with a clear inline validation message. Given 5 failed payment attempts from the same IP or device within 60 seconds, When another attempt is initiated, Then a bot check challenge is shown and further attempts are rate‑limited to 1/minute for the next 10 minutes. Given a successful payment confirmation, When the checkout returns, Then a success state displays within 2 seconds and the progress meter updates in real time.
Completion and Expiry State Transitions
Given the mini‑goal is reached (friends ≥ F and dollars ≥ D), When the page renders, Then the meter displays 100% with a “Goal met—thank you!” message and the primary CTA switches to “Keep Giving” with a secondary “Share” action. Given the mini‑goal is exceeded, When the page renders, Then the meter remains at 100% and an “+over goal” badge shows the overage. Given the sprint has expired, When the page renders, Then the meter shows an “Ended” state, the Donate CTA is disabled or rerouted to the general donate page with explicit copy indicating the sprint ended, and sharing reflects that the sprint has ended.
Localization and Currency Formatting
Given locale is set to en‑US and currency to USD, When the page renders, Then all copy is English and money formats as $1,234.56. Given locale is set to es‑ES and currency to EUR, When the page renders, Then all copy is Spanish and money formats as 1.234,56 €. Given an unsupported locale is requested, When the page renders, Then the UI falls back to en‑US with the campaign’s configured currency format. Given the locale is changed via a ?lang parameter, When the page reloads, Then all translatable strings update to the selected locale and numerals/currency reformat accordingly.
Accessibility (WCAG 2.2 AA) Compliance
Given keyboard‑only navigation, When tabbing through the page, Then all interactive elements are reachable in logical order with a clearly visible focus indicator. Given a screen reader user, When the page loads, Then the progress meter exposes role=progressbar with aria‑valuenow, aria‑valuemax, and has an aria‑live region that announces updates when donations post. Given color contrast testing, Then text/background pairs meet ≥ 4.5:1 and UI components/icons meet ≥ 3:1. Given a mobile viewport, Then all actionable targets have a minimum hit area of 44×44 CSS pixels. Given user prefers reduced motion, When animations would play, Then motion is reduced or disabled without loss of information.
Open Graph and Social Preview Metadata
Given a valid share token, When a social crawler requests the URL, Then og:title, og:description, og:image, og:url, og:type, and twitter:card/meta tags are present and populated without exposing PII beyond ambassador first name or “Anonymous”. Given an invalid or expired token, When a crawler requests the URL, Then generic, non‑personalized preview metadata is returned. Given the og:image is generated, When fetched, Then it is at least 1200×630 pixels, <300 KB, and includes organization branding with legible text. Given the metadata is validated, When tested in Facebook Sharing Debugger and X Card Validator, Then no errors or critical warnings are reported and previews render as intended.
Privacy-Safe Conversion Attribution & Consent
"As a privacy-conscious donor, I want my contribution attributed correctly without exposing my personal details so that I feel safe participating."
Description

Attribute conversions to the ambassador using privacy-preserving link tokens without third-party cookies, and never reveal donor PII to the ambassador unless explicitly consented. Provide clear consent controls for donors to display first name/initial on the meter or remain anonymous, along with organization-configurable default settings and copy. Maintain audit logs, signed tokens, rate limits, and configurable retention to meet GDPR/CCPA obligations. Expose aggregated, anonymized attribution data to downstream analytics and the Impact Board.

Acceptance Criteria
Tokenized Share Link Attribution Without Third-Party Cookies
Given an ambassador generates a Share Sprint link after a top-up When the link contains a signed, opaque token with no PII and is opened in a browser with third-party cookies disabled Then attribution of any completed donation within the same session is credited to the ambassador using only the URL token and first-party storage, and no third-party cookies are set And if the token is missing or fails signature verification, no attribution is recorded and the event is audit logged with reason "invalid_token" And the token payload contains no donor or ambassador PII (only opaque ID and signature) as verified by inspection tests
Donor Consent UI and Org-Configurable Defaults for Name Display
Given the donation form is loaded via an ambassador link When the consent control renders Then the default selection and display copy match the organization’s configured values And selecting "Show first name/initial" previews the exact meter display and persists only upon form submission When the donor submits with "Anonymous" Then no name or initials appear to the ambassador or on the meter, and consent is stored as Anonymous with timestamp and copy version When the donor submits with "Show first name/initial" Then the first name/initial appears on the meter and to the ambassador, and consent choice, timestamp, and copy version are stored
Ambassador Views Respect Donor PII Consent Boundaries
Given an ambassador views their Share Sprint performance When donations include a mix of consented and non-consented donors Then only consented donors’ first name/initial are shown; all others appear as "Anonymous" And no email, phone, or other PII is visible or exportable to the ambassador And attempts to access donor details via client/API without admin scope return 403 and are audit logged
Aggregated, Anonymized Attribution for Analytics and Impact Board
Given downstream analytics and the Impact Board request attribution data When the data feed is generated Then only aggregated metrics (e.g., clicks, conversions, total amount) by ambassador and time window are included with no donor identifiers And the schema excludes PII fields and passes JSON schema validation And totals reconcile with internal reports within ±0.5% over a 24-hour window And a consent-rate metric is included for transparency without exposing individual choices
Signed Tokens with Immutable Audit Logging
Given a token is generated, clicked, or redeemed When each event occurs Then an immutable audit log entry is created with timestamp, ambassador ID, token ID, event type, IP hash, and outcome And any token alteration results in signature verification failure and blocks attribution while logging the incident And admins can query audit logs by ambassador and date range within 2 seconds for 95th percentile requests
Rate Limiting and Abuse Controls Preserve UX
Given rate limits are configured (e.g., 100 link opens/min per IP and 20 attributions/min per token) When traffic exceeds a configured threshold Then excess events are rejected with 429-equivalent responses, attribution is not recorded, and incidents are audit logged And the donation page still renders and permits donation submission without attribution errors for legitimate users And false-positive rate under synthetic high-load tests is ≤1%
Data Retention, Erasure, and Consent Withdrawal Compliance
Given the organization sets raw attribution retention to 90 days When the retention job runs after 90 days Then raw attribution events older than 90 days are purged while aggregated metrics remain intact When a donor requests erasure or withdraws consent Then their PII is removed or hidden from ambassador views and meters within 7 days, past displays update to "Anonymous," and an audit log entry is created And admins can export current consent records in a machine-readable format on demand
Ambassador Auto-Nudges & Progress Notifications
"As an ambassador, I want timely progress updates and prompts so that I know when to re-share and hit my mini-goal."
Description

Deliver timely, configurable nudges to ambassadors about sprint progress (first gift, halfway, near-expiry, completion) via in-app notifications and email, respecting quiet hours and user preferences. Include one-tap re-share actions preloaded with the best-performing template and link. Cap frequency to prevent spam, track engagement, and suppress further messages after completion or opt-out. Localize message copy and reuse the existing notifications infrastructure.

Acceptance Criteria
Milestone-Triggered Ambassador Nudges
Given an active Share Sprint with an assigned ambassador When the first donation attributed to the ambassador’s link is received Then send an in-app notification and an email within 2 minutes including the sprint name, current progress, and a primary CTA to re-share Given an active Share Sprint When progress first crosses 50% of the ambassador’s mini-goal Then send an in-app notification and an email within 5 minutes including a progress meter and re-share CTA Given an active Share Sprint with an expiry time When there are 3 hours remaining and the mini-goal is incomplete Then send an in-app notification and an email within 5 minutes including time remaining and re-share CTA Given an active Share Sprint When the ambassador completes the mini-goal Then send an in-app notification and an email within 2 minutes including a completion message and suppress all further nudges for that sprint
Quiet Hours and Time Zone Compliance
Given org-level quiet hours are 21:00–08:00 local time and the user’s time zone is set When a nudge is triggered during quiet hours Then do not send email and do not display an in-app notification; schedule delivery for 08:00 local time Given a user without a stored time zone When a nudge is triggered Then resolve time zone from device (in-app) or org default (email) before applying quiet hours Given a nudge queued due to quiet hours When the permissible window opens Then deliver email within 10 minutes and make the in-app notification visible on the user’s next app open after that time Given quiet hours are changed by an admin While nudges are queued Then reschedule any queued sends that now conflict to the next permissible window
Channel Preferences, Opt-Out, and Suppression
Given a user has email disabled for Ambassador Nudges When a milestone triggers Then send only an in-app notification and do not send email Given a user has in-app nudges disabled When a milestone triggers Then send only email and do not create an in-app notification Given a user clicks Unsubscribe from Ambassador Nudges in any email When confirmation is submitted Then suppress all future emails for this category within 1 minute and present a confirmation screen Given an ambassador toggles off Ambassador Nudges in Notification Settings When saved Then suppress both in-app and email nudges for all current and future sprints Given a sprint is marked complete for an ambassador When any further triggers occur for that sprint Then do not send any nudges and record a suppressed_due_to_completion reason
Frequency Capping and De-duplication
Given default caps of max 3 nudges per 24 hours and a minimum 60 minutes between nudges per ambassador per sprint When multiple triggers occur Then send only the first eligible nudge and suppress additional nudges per cap policy Given two identical milestone events occur within 10 minutes due to retries or duplicates When processing Then deduplicate by ambassador+sprint+event type within a 15-minute idempotency window Given org-level cap settings are updated When saved Then apply the new caps to subsequent deliveries without altering already-sent or already-suppressed records Given an ambassador has reached the daily cap When another trigger occurs within the same 24-hour window Then do not send and log a suppressed_due_to_cap event with timestamp and trigger type
One-Tap Re-share with Best-Performing Template
Given a nudge is generated When selecting the CTA content Then choose the template variant with the highest 30-day conversion rate for the org (min 100 sends); if not met, use the global best-performing variant; if no data, use default variant A Given a nudge is delivered When the ambassador activates the CTA (tap in-app or click in email) Then open the share surface with prefilled template text and the ambassador’s unique sprint link and complete the re-share in ≤2 taps Given a template variant is updated in the Template Library When the next nudge is composed Then use the updated content without requiring an app update Given a re-share is initiated from a nudge When attributing downstream actions Then tag donations and signups with ambassador_id, sprint_id, template_id, and UTM parameters
Localization and Copy Fallback
Given the user’s preferred language is supported (e.g., en, es) When sending a nudge Then render subject, body, CTA, and placeholders in that language with correct pluralization Given the user’s preferred language is not supported When sending a nudge Then fall back to English while preserving all dynamic placeholders Given a right-to-left language is selected When rendering a nudge Then align text RTL where applicable and format numbers per locale Given any translation key is missing for a selected template variant When rendering Then log a localization_missing event and fall back to the default language for that key only
Engagement Tracking and Infrastructure Reuse
Given any nudge is sent When delivery occurs Then create a record via the existing Notifications Service including message_id, sprint_id, ambassador_id, template_id, channel, trigger_type, and timestamps Given a nudge is viewed in-app When opened Then record an open event; for email, record an open when the tracking pixel loads; always record CTA clicks with unique click_id Given donations or signups occur within 7 days of a re-share click When attribution rules match Then increment conversions for the originating nudge and template variant Given a provider delivery failure occurs When the infrastructure retries per policy Then record final status as delivered, bounced, or failed with error code Given the analytics endpoint is queried for an ambassador’s sprint When requested Then return counts for sent, opens, clicks, re-shares, and conversions with timestamps and breakdown by channel
Impact Board Aggregation & Reporting
"As an organizer, I want Share Sprint results to roll up into the Impact Board so that I can see what’s working and optimize campaigns."
Description

Aggregate Share Sprint metrics into the Impact Board and admin analytics, including sprints started/active/completed, friends converted, dollars raised, and channel performance by campaign/time/team. Provide real-time tiles and export/API access for deeper analysis. Ensure deduplication, single-source-of-truth attribution, and performant queries with caching. Support filters and segments to surface what’s working and where to focus outreach.

Acceptance Criteria
Real-time Impact Board Tile Updates
Given a Share Sprint event (sprint_started, donation_attributed, friend_converted, sprint_completed) is recorded When the Impact Board is viewed Then the corresponding tiles reflect the new counts and amounts within 5 seconds for 95% of events and within 15 seconds for 99.9% of events And the "Last updated" timestamp shows the update time to the nearest second And tile totals exactly match aggregated source-of-truth events as of the refresh time
Deduplication and Last-Touch Attribution
Given multiple clicks across channels to different Share Sprint links by the same friend within a 7-day attribution window And the friend completes one or more donations When attribution is computed Then exactly one conversion is counted per friend per sprint (no double counting) And dollars raised are attributed once to the last-touch sprint/link within the window And retries, duplicate webhooks, or repeated form submissions do not create duplicate conversions or donations (idempotent processing) And each attributed record includes sprint_id, ambassador_id, channel, campaign_id, and attribution_model = "last_touch_7d"
Filterable Channel/Campaign/Time/Team Analytics
Given filters for campaign(s), date range, channel(s), team(s), and ambassador(s) When filters are applied in any combination Then totals, breakdowns, and charts reflect the intersection of selected filters And channel/campaign/time/team breakdowns sum exactly to the filtered totals And cached queries return within 2 seconds (p95) and cold queries within 6 seconds (p95) on a dataset of 10M events And results in Impact Board and Admin Analytics views are numerically identical for the same filters
Sprint Lifecycle and Mini-goal Progress Aggregation
Given a Share Sprint with defined personal goals (friends_target and/or dollars_target) and an end_at timestamp When unique friends convert and donations are attributed Then progress displays friends_converted/target and dollars_raised/target with percentages rounded to 1 decimal And status is "Started" when link is created, "Active" when within start..end and target not met, and "Completed" when target met or end_at passed And counts of sprints started/active/completed on the Impact Board equal the sum of sprints in each status for the selected filters
Exports and API Parity for Impact Metrics
Given a selected filter set and date range When the user exports CSV or accesses the /analytics/share-sprint endpoint Then both outputs contain identical rows and columns with matching totals and attribution fields (sprint_id, ambassador_id, channel, campaign_id) And timestamps are ISO 8601 UTC with timezone "Z" And pagination and row counts are deterministic (page_size, next_cursor) with no missing/duplicate rows across pages And exports up to 1,000,000 rows complete within 60 seconds; API responses return within 5 seconds (p95) for pages up to 10,000 rows
Caching, Data Freshness Indicator, and Manual Refresh
Given tile and analytics caching with TTL = 30 seconds When new events arrive during the TTL Then the UI shows a "Data last updated" timestamp and a "Refresh" control And tapping "Refresh" bypasses cache and returns updated values within 6 seconds (p95) And a "stale" indicator is shown if data age > 60 seconds
Late-arriving Events, Backfill, and Auditability
Given events may arrive late or be corrected within a 48-hour window When late or corrected events are ingested Then impacted aggregates are re-computed and visible in tiles, analytics, exports, and API within 2 minutes And prior values and the change are recorded in an audit log with who/when/what for traceability And replays or backfills do not create duplicate conversions or donations (idempotent upsert by event_id)

Sticky Preferences

Auto-remembers each volunteer’s last confirmed accommodations and pre-fills the 3‑tap SMS prompt for the next shift. Volunteers just confirm or adjust in seconds, reducing survey fatigue and missed needs. Captains see early counts (e.g., seating, ASL, scent-free) to stage resources ahead of time, all while honoring consent scope across partners.

Requirements

Preference Capture & Storage Model
"As a volunteer, I want my previously confirmed accommodations to be remembered per partner so that I can confirm quickly without re-entering details."
Description

Design and implement a normalized data model to persist each volunteer’s last confirmed accommodations per partner and per context (e.g., event type or location), including timestamp, source channel, shift ID of confirmation, and consent scope reference. Support multi-select accommodations (e.g., seating, ASL, scent-free), free-text notes when allowed, and default values when no history exists. Provide CRUD APIs and mobile/offline-safe caching so the next-shift prompt can prefill instantly, with encryption at rest and in transit, field-level privacy classifications, and row-level scoping by partner. Include migration scripts for existing records and idempotent upserts to prevent duplication when confirmations are retried.

Acceptance Criteria
Partner-Isolated Preference Retrieval
Given volunteer V has saved accommodations with partner A and partner B And the API request includes partnerId=A and an access token scoped to partner A When GET /v1/preferences?volunteerId=V&partnerId=A Then only records where partnerId=A are returned And no records for partnerId=B are included And the response status is 200 Given the same request is made with an access token scoped to partner B When GET /v1/preferences?volunteerId=V&partnerId=A Then the response status is 403 And zero data from partner A is returned Given a request omits partnerId When GET /v1/preferences?volunteerId=V Then the response status is 400 And an error message states partnerId is required
Context-Aware Prefill and Offline Instant Load
Given V has a last confirmed accommodations record for partner A with context {eventType=Phonebank, locationId=HQ} When the mobile app generates the next-shift prompt for a new shift with partner A and context {eventType=Phonebank, locationId=HQ} Then the prefill matches the last confirmed accommodations exactly And includes the stored notes (if policy allows) Given no matching context-specific record exists but a partner-level default exists When generating the prompt Then the prefill uses the partner-level default Given neither context-specific nor partner-level records exist When generating the prompt Then the prefill uses system defaults configured for partner A Given the device is offline with a warm cache When opening the next-shift prompt Then the prefill is rendered in ≤200ms from local encrypted cache And the local cache is encrypted using OS keystore-backed storage And upon reconnect, the cache sync updates the record if a newer server version exists without creating duplicates
CRUD API and Metadata Persistence
Given a POST to /v1/preferences with {volunteerId, partnerId, context, accommodations[], notes?, shiftId?, sourceChannel, consentScopeId} When the payload is valid Then response is 201 with resource id and ETag And the stored record contains: - updatedAt in UTC ISO 8601 with ms precision - sourceChannel in {SMS, Web, Admin, API} - shiftId populated when sourceChannel is SMS/Web for a shift; null otherwise - valid consentScopeId referencing an existing consent record Given a GET /v1/preferences/{id} Then the response includes all persisted fields above and matches the stored values Given a PATCH /v1/preferences/{id} with If-Match ETag When the ETag matches Then the update succeeds with 200 and a new ETag When the ETag does not match Then 409 Conflict is returned and no update occurs Given a DELETE /v1/preferences/{id} with proper authorization Then 204 No Content is returned And subsequent GET returns 404
Idempotent Upsert on Confirmation Retries
Given two identical confirmation payloads are received within 48 hours for the same composite key {volunteerId, partnerId, context, consentScopeId, shiftId} When processed via POST /v1/preferences/confirmations with Idempotency-Key header Then exactly one logical record exists And the API returns 200/201 consistently with the same resource id for both attempts Given a second payload differs and has a newer confirmedAt timestamp than the stored record When processed Then the existing record is updated in place (no duplicate row) And updatedAt reflects the newer timestamp Given a second payload differs but has an older confirmedAt timestamp When processed Then the existing record is not modified And the API returns 200 with unchanged resource id and a reason "stale confirmation"
Multi-Select Accommodations and Conditional Notes
Given partner A has an allowed accommodations catalog {seating, ASL, scent_free} When a confirmation includes accommodations ["seating","ASL"] Then the record stores the set without duplicates And order does not affect equality Given a confirmation includes an accommodation not in the catalog Then the API returns 422 with details for the invalid code Given partner A allows free-text notes up to 280 characters When a confirmation includes notes <= 280 characters Then notes are stored and retrievable Given notes exceed 280 characters or contain disallowed content per policy (e.g., HTML/script) When submitted Then the API returns 422 and does not persist the notes
Security and Privacy Enforcement
Given data is stored When inspected at rest Then fields classified Sensitive (e.g., notes, accommodations) are encrypted with AES-256 and keys managed by KMS And all API endpoints require TLS 1.2+ in transit Given a user with role Captain requests a record Then fields classified Restricted are redacted unless the role includes permission "view_restricted_fields" Given a user with role PrivacyOfficer requests the same record Then all fields are visible Given server logs and exports are generated Then fields classified Sensitive are masked or omitted per classification policy
Migration and Backfill of Existing Records
Given an existing dataset of N legacy preference entries When the migration tool is run in dry-run mode Then it outputs a report with counts of rows to migrate, duplicates to merge, and validation errors without modifying production data Given the migration is executed in live mode Then at least 99.9% of valid legacy rows are migrated And duplicates are consolidated using composite key {volunteerId, partnerId, context, consentScopeId, shiftId} And a reconciliation report shows: total legacy rows, migrated rows, merged duplicates, rejected rows with reasons And the process is idempotent so re-running yields zero additional changes
Auto-Prefill SMS Prompt with Quick Adjust
"As a volunteer, I want an SMS prompt pre-filled with my usual accommodations so that I can confirm or adjust in seconds before my next shift."
Description

Generate and send a 3-tap SMS microsurvey pre-filled with the volunteer’s last confirmed accommodations, enabling single-tap confirm and quick adjust via reply keywords or short deep links. Handle carrier-safe message lengths, localization, time‑zone aware scheduling, and failovers (e.g., if deep link fails, fall back to keyword menu). Validate inputs server-side, write-back confirmed selections atomically, and emit events for the counts dashboard. Respect quiet hours and opt-out lists, deduplicate prompts per shift, and expose configuration for cadence and reminder retries.

Acceptance Criteria
Pre-Filled 3-Tap SMS Confirm
Given a volunteer has last confirmed accommodations on file and an upcoming shift, and the volunteer is not opted out and is within allowed send window When the system generates the pre-shift SMS prompt Then the SMS includes a concise summary of the last confirmed accommodations And the SMS includes a single-tap confirm action (reply keyword "C" or confirm link) And a unique prompt ID is assigned and correlated to the volunteer and shift And the outbound message is recorded in the delivery log When the volunteer confirms via the confirm action Then the server validates the prompt ID, volunteer, and shift association And the confirmation is written with timestamp and shift ID And the volunteer’s last_confirmed_accommodations is updated to the confirmed set And an acknowledgment SMS is sent to the volunteer
Quick Adjust via Reply Keywords
Given a volunteer has received the pre-filled SMS prompt containing the keyword menu When the volunteer replies with a supported keyword/value pair (e.g., "ASL Y", "SEAT 1", "SCENT FREE") Then the server validates the keyword and value against the allowed accommodations set And the change is applied to the current shift selection and last_confirmed_accommodations And an SMS confirmation summarizing the updated selections is sent And leading/trailing whitespace and case differences in the reply are ignored When the volunteer sends an invalid or unsupported keyword/value Then the system responds with a concise help message and examples And no changes are persisted And after 3 invalid attempts, the full keyword menu is resent
Quick Adjust via Deep Link with Fallback
Given a volunteer taps the short deep link in the prompt When the link resolves successfully Then a mobile micro-form opens pre-populated with the last confirmed accommodations And the volunteer can submit updated selections in no more than 3 taps And upon submit, the server validates and updates the selections And a confirmation SMS with a summary of selections is sent When the deep link fails to resolve (non-2xx or timeout) Then the system automatically sends the keyword menu fallback within 15 seconds And the failure and fallback are logged with correlation to the prompt ID
Carrier-Safe Message Length Handling
Given dynamic content (accommodations summary, keywords, links) is assembled for an SMS prompt When preparing the message for send Then GSM-7 encoded messages are limited to ≤306 characters (≤2 segments) and UCS-2 to ≤268 characters (≤2 segments) And non-critical copy is truncated before action keywords and the deep link are removed And a compact template variant is used automatically if the first render exceeds the segment limits And the final encoding, character count, and segment count are logged per message
Localization of Prompts and Keywords
Given a volunteer’s preferred locale is available (else default to en-US) When generating the SMS prompt and any deep-link micro-form Then all static text, date/time formats, and accommodation labels are localized to the volunteer’s locale And localized reply keywords are accepted in addition to en-US equivalents And the deep-link micro-form opens in the same locale as the SMS And if the locale is unsupported, the system falls back to en-US and logs the fallback
Time-Zone Aware Scheduling with Quiet Hours
Given a shift scheduled at time S and a volunteer with time zone TZ and configured cadence K and retries R When scheduling the initial prompt Then the send time is S − K converted to TZ and placed within the allowed send window And no messages are sent during configured quiet hours in TZ; sends are deferred to the next allowed window And daylight saving time transitions in TZ are handled correctly And all scheduled and actual send times are stored in UTC with TZ offset metadata When a scheduled send falls into quiet hours Then it is automatically rescheduled to the next allowed window without exceeding S (shift start)
Validation, Atomic Write-Back, Idempotency, Events, Dedup, Opt-Out, and Retries
Given an incoming SMS reply or deep-link submission referencing a prompt ID When processing the update Then the server validates volunteer identity, shift eligibility, and field constraints And the write is performed atomically so either all fields update or none do And processing is idempotent so duplicate replies with the same prompt ID do not create duplicate updates or events And an accommodations_confirmed event with counts deltas is emitted to the counts dashboard stream within 2 seconds of write completion And only one active prompt per volunteer per shift exists; retries reference the same prompt ID and do not create duplicates And phone numbers on global or org opt-out lists or with hard carrier blocks are excluded from sends and processing; attempts are logged And retry behavior (max attempts, delay/backoff) is configurable; retries stop upon confirmation or receipt of STOP
Consent Scope & Expiration Enforcement
"As a volunteer, I want my preferences used only where I consented so that my information is respected and not shared across partners without permission."
Description

Enforce consent boundaries by storing the consent text version, capture timestamp, scope (organization/partner/event), and expiration for accommodations usage. Prior to prefill or aggregation, verify active consent for the relevant scope; if missing or expired, prompt for consent refresh before using prior selections. Support granular revocation, partner isolation, and audit trails for consent checks, with privacy-first defaults that suppress cross-partner reuse and exclude non-consented data from analytics. Provide admin tools and APIs to view, renew, and revoke consent.

Acceptance Criteria
Prefill Requires Active In-Scope Consent
Given a volunteer has last-confirmed accommodations for Partner A and an active consent record (scope=partner:A, expires_at > now, consent_text_version present) When the system composes the 3‑tap SMS prompt for a Partner A shift Then the prompt is prefilled with the last-confirmed accommodations, no consent renewal is shown, and an audit check is recorded with outcome=allowed Given a volunteer has last-confirmed accommodations for Partner A but no active consent for scope=partner:A (missing or expired) When the system composes the 3‑tap SMS prompt for a Partner A shift Then the prompt is not prefilled, a consent request referencing Partner A and the current consent_text_version is shown, submission is blocked until consent is accepted, and an audit check is recorded with outcome=blocked and reason=missing_or_expired_consent
Consent Expiration Blocks Use
Given a consent record with expires_at <= now for scope=partner:A When generating SMS prefill or captain early counts for Partner A Then the system treats consent as inactive, does not prefill, excludes the volunteer from counts, and initiates a consent renewal prompt Given the volunteer accepts the renewal When consent is captured Then a new consent record is stored with captured_at (now), current consent_text_version, updated expires_at, and subsequent requests prefill/include the volunteer as expected
Granular Revocation Per Scope
Given a volunteer has active consent for Partner A and Partner B When the volunteer revokes consent for Partner A Then all subsequent prefill, analytics, and exports for scope=partner:A are blocked/excluded, while Partner B usage continues unaffected, and an audit record with action=revoke and scope=partner:A is appended Given any API call attempts to prefill or aggregate for Partner A after revocation When the request is processed Then the API responds 403 with code=CONSENT_REVOKED and the UI displays the consent-required flow
Partner Isolation & Analytics Suppression
Given a volunteer has active consent for Partner B only When a Partner A captain views early accommodation counts Then the volunteer’s data is excluded and no cross-partner reuse occurs by default Given analytics dashboards or the Impact Board aggregate accommodations When computing metrics Then only records with active consent for the same scope are included; non-consented or out-of-scope records are excluded from all aggregates and exports
Consent Audit Trail & Check Logging
Given any consent capture, renewal, revocation, or usage check occurs When the event is processed Then an immutable audit record is appended with fields: volunteer_id, scope_type (org|partner|event), scope_id, consent_text_version, action (capture|renew|revoke|check), outcome (allowed|blocked for checks), reason_code (if blocked), actor_id (or system), performed_at (ISO‑8601), and request_id Given an admin requests an audit export filtered by date range, volunteer, and scope When the export is generated Then the file contains only matching records with complete fields, preserving original values with no edits or deletions
Admin & API Consent Management
Given an authorized admin (Org Admin or Partner Captain within scope) When viewing a volunteer’s consent panel Then they can see consent records by scope with captured_at, consent_text_version, and expires_at, and can initiate renew or revoke per scope with required reason Given API consumers query consent When calling GET /consents?volunteer_id={id}&scope={org|partner|event} Then the API returns the current consent status and metadata for that scope only, enforcing caller scope permissions Given a client attempts prefill or aggregation without active consent When calling the relevant API Then the API responds 403 with code=CONSENT_REQUIRED or CONSENT_EXPIRED; all such responses are logged in the audit trail
Scope Resolution Priority (Org/Partner/Event)
Given an action occurs within an event E under Partner A When evaluating consent Then the system uses the most specific active consent available in this order: event E > partner A > organization; if none are active, usage is blocked and renewal is prompted Given a consent captured for Partner B only When evaluating usage for Partner A Then the Partner B consent is not considered valid for Partner A and usage is blocked until Partner A consent is active
Captain Early Counts Dashboard
"As a captain, I want early aggregated counts of accommodations for upcoming shifts so that I can stage resources like seating or interpreters ahead of time."
Description

Provide captains with a privacy-preserving early counts view of upcoming shifts that aggregates accommodations (e.g., seating, ASL, scent-free) from confirmed responses in near real time. Offer filters by date range, event, location, and partner scope, show confidence levels and last refresh time, and surface deltas versus expected capacity to guide resource staging. Exclude any personally identifiable details, enforce minimum cohort thresholds to avoid re-identification, and export counts to the Impact Board and CSV. Ensure sub-second load for typical queries and background jobs to keep aggregates updated.

Acceptance Criteria
Privacy-Preserving Aggregated Counts Display
Given upcoming shifts have confirmed accommodation responses within the selected scope When a captain opens the Captain Early Counts Dashboard Then only aggregated counts per accommodation type are displayed and no personally identifiable information (name, phone, email, identifiers, notes) appears anywhere on the dashboard Given any filtered cohort for an accommodation type has fewer than 5 confirmed responses When the captain views the corresponding cell Then the numeric count is suppressed, the cell shows "Suppressed", and the value is excluded from totals and exports Given multiple accommodation types have sufficient cohort size (>= 5) When displayed together Then their totals equal the sum of the visible per-shift counts for that accommodation within the active filters
Multi-Filter Controls (Date, Event, Location, Partner Scope)
Given the dashboard is loaded When the captain applies filters for date range, event, location, and partner scope Then results include only shifts matching all selected filters (logical AND) and exclude all others Given no filter is set for a dimension When the query runs Then that dimension is unbounded (matches all values) Given partner scope X is selected and consent constraints exist When counts are computed Then only responses from volunteers whose consent permits inclusion under partner scope X are counted; all others are excluded from aggregates
Confidence Levels and Last Refresh Visibility
Given expected capacity and confirmed counts exist for a filtered view When confidence is displayed Then confidence is computed as coverage = min(confirmed_count / expected_capacity, 1.0) and labeled High (>= 70%), Medium (40%–69%), or Low (< 40%); if expected_capacity is 0 or missing, confidence shows "Unknown" Given the dashboard loads or the captain clicks Refresh When aggregates are retrieved Then a "Last refreshed" timestamp is shown in the user's timezone with minute precision and matches the server refresh within ±5 seconds
Deltas Versus Expected Capacity Calculation
Given expected capacity is configured per accommodation at the event or location level When the dashboard renders counts Then delta is displayed per accommodation as delta = confirmed_count − expected_capacity with formatting: negative prefixed with "−", positive with "+", zero as "0" Given expected capacity is missing for an accommodation When the row renders Then delta shows "N/A" and is excluded from any delta totals
Near Real-Time Updates via Background Aggregation
Given a volunteer confirms or updates an accommodation via SMS When the change is saved to the system Then the corresponding aggregate used by the dashboard is updated by the background job within 60 seconds at the 95th percentile, and a manual Refresh on the dashboard reflects the change within 2 seconds after the aggregate updates Given multiple updates occur within one minute When background processing runs Then aggregates remain consistent (no double-counting or omission) and the Last refreshed timestamp advances to the most recent successful aggregation time
Export to Impact Board and CSV (No PII)
Given the captain clicks Export → Impact Board with active filters When the export completes Then the Impact Board shows a new snapshot within 30 seconds containing accommodation counts, deltas, confidence labels, last refreshed time, and filter context; no personally identifiable information is included Given the captain clicks Export → CSV When the file is downloaded Then the CSV is UTF-8 encoded and contains columns: date, event, location, partner_scope, accommodation_type, confirmed_count, expected_capacity, delta, confidence, last_refreshed_at; cohorts under threshold show "Suppressed" in confirmed_count and delta, and no PII fields are present; generation completes within 2 seconds for up to 10,000 rows
Sub-Second Load for Typical Queries
Given a typical query (date range ≤ 30 days, ≤ 100 upcoming shifts, ≤ 10 events, one location, one partner scope) When the dashboard is loaded or filters are applied Then the aggregated results are rendered in under 1.0 second at the 95th percentile measured from request to rendered counts, with the aggregation API responding in ≤ 700 ms p95
Accommodation Type Configuration & Localization
"As an admin, I want to configure which accommodation options appear and how they’re worded so that prompts are accurate, inclusive, and localized."
Description

Allow admins to configure the set, labels, order, and grouping of available accommodation options globally and with partner or event-level overrides. Support synonyms and shortcodes for SMS replies, locale-specific translations, accessibility-conscious copy, and conditional visibility (e.g., only show ASL when interpreter resources exist). Validate configurations for conflicts, version changes, and migration of existing user data, and expose configuration via UI and API with role-based access control.

Acceptance Criteria
Admin configures global accommodation set
- Given I am a Super Admin on the Accommodations screen, When I add, rename, reorder, group, or archive accommodation types and save, Then the preview reflects the exact order and grouping and the changes persist on refresh. - Given I attempt to save with duplicate type keys, duplicate labels within the same group, or empty labels, When I submit, Then I see inline validation errors and the configuration is not saved until resolved. - Given I archive a type in the global set, When saved, Then the type becomes hidden in all downstream UIs and APIs but remains referenceable for historical data and reporting. - Given I reorder groups and types, When saved, Then SMS prefill order matches the configured order.
Partner override falls back to global
- Given a Partner Admin opens their partner override, When they override only a subset of types (add, rename, hide), Then non-overridden types inherit from the current global configuration. - Given a volunteer associated with that partner, When they receive an SMS prompt, Then only the partner’s visible types appear with partner-specific labels and order. - Given a volunteer not associated with that partner, When they receive an SMS prompt, Then the global set is used. - Given a type is hidden by a partner, When existing user data references that type, Then historical data remains intact and is mapped to a “hidden” state without exposing the option in prompts.
Event-level conditional visibility based on resources
- Given an Event Captain marks ASL resources as unavailable for an event, When the SMS prompt is generated, Then the ASL accommodation is excluded from the prompt and the captain’s counts indicate “ASL unavailable.” - Given resources later become available before prompts are sent, When the event setting is updated, Then the ASL option appears in the next generated prompts and in the volunteer portal. - Given an option is conditionally hidden, When an API consumer requests the event’s configuration, Then the response includes the option with visibility=false and the reason code.
Synonyms and SMS shortcodes mapping with conflict validation
- Given Admins configure synonyms and shortcodes per type and locale, When a volunteer replies via SMS with any configured synonym/shortcode (case- and diacritic-insensitive), Then it maps to the correct type value. - Given two types share a synonym/shortcode in the same locale or a shortcode conflicts with reserved commands (e.g., STOP, HELP), When saving, Then validation blocks the save with a clear conflict message. - Given a volunteer reply matches multiple synonyms ambiguously, When processed, Then the system sends a disambiguation prompt and does not commit a value until clarified. - Given a volunteer replies with multiple comma- or space-separated shortcodes, When processed, Then the system records all matched multi-select types and rejects unknown tokens with a summary message.
Localization and accessibility-conscious copy
- Given the org’s default locale and the volunteer’s preferred locale are set, When a prompt is rendered, Then labels and synonyms are pulled from the volunteer’s locale, falling back to the org default if missing. - Given RTL locales, When prompts and UI are rendered, Then layout and punctuation are directionally correct and screen readers announce group and type labels in the correct reading order. - Given accessibility copy guidelines, When labels are saved, Then they pass checks for plain language, avoid ambiguous abbreviations, and provide aria-label or tooltip context where needed, with inline guidance on violations. - Given truncated SMS limits, When a prompt is composed, Then the system ensures the message fits within the configured segment budget by truncating descriptions but not labels and records the final segment count.
Versioning, publishing, and migration of existing data
- Given a Draft/Publish workflow, When an Admin saves changes as Draft, Then the live configuration remains unchanged and the draft is previewable. - Given the Admin publishes, When published, Then a new configuration version is created with an audit entry (who, when, diff) and the API/UI start serving the new version. - Given types are renamed, merged, or split, When publishing, Then a migration map is required and validated, and existing volunteer preferences are migrated with a summary report of mapped/unmapped records. - Given a rollback is needed, When the Admin selects a prior version and confirms, Then the system reverts to that version and does not re-run migrations, preserving historical mappings.
Role-based access control and API exposure
- Given RBAC, When an Org Admin authenticates, Then they can create/update/publish global configurations; Partner Admins can create/update partner overrides for their partner only; Event Captains can toggle event resource flags only; Volunteers have read-only access to their own prompts. - Given API endpoints, When called with appropriate scopes, Then GET returns effective configuration by context (global, partner, event) and POST/PUT/PATCH are restricted per role; unauthorized requests receive HTTP 403 with error code. - Given every change, When saved, Then an immutable audit log entry is recorded including user, timestamp, context (global/partner/event), and change summary, and is retrievable via API by Org Admins.
Audit Logging & Data Retention Policy
"As a compliance lead, I want audit logs and retention controls for accommodation data so that we meet privacy obligations and can respond to data requests."
Description

Record immutable audit logs for all reads and writes to accommodation preferences and consent artifacts, including actor, timestamp, scope, and before/after values. Implement retention policies that purge or anonymize data per jurisdictional requirements (e.g., 12–24 months), with configurable TTLs, bulk deletion tools, and legal hold support. Provide export tooling for subject access requests and incident response, and include monitoring and alerts for unusual access patterns.

Acceptance Criteria
Immutable Write Audit Logs for Accommodations and Consent Changes
Given a captain, volunteer, or system process updates a volunteer’s accommodation or consent record When the write operation is committed Then an append-only audit entry is recorded with actor ID, role, request ID, UTC ISO-8601 timestamp, record type, record ID, consent scope, field-level before and after values, originating IP, user agent, and outcome (success/failure) And the audit entry includes a tamper-evident hash and prev_hash to form a verifiable chain And privileged users cannot alter or delete audit entries; attempts are rejected and logged as security events And secrets or tokens appearing in values are masked per policy while preserving diff visibility
Read Access Audit Logs for Accommodations and Consent
Given any user or service reads accommodations or consent artifacts via UI or API When the response is served Then an audit entry is recorded with actor ID, role, request ID, UTC timestamp, record type, record identifier(s) or query parameters, list of fields returned (not values), purpose tag, consent scope, originating IP, user agent, and outcome And bulk reads create one request-level summary (row count, filter) plus per-record entries batched up to 1,000 items per minute And requests outside consent scope are denied with 403 and an audit entry is recorded indicating scope violation And if audit recording fails, the read is denied and a high-severity incident is logged within 1 minute
Configurable Retention TTL by Jurisdiction with Purge/Anonymize
Given retention policies are configured per jurisdiction and data type (e.g., accommodations: 12 months CA, 24 months US; audit logs: 24 months) When the scheduled retention job runs nightly Then records older than their TTL are purged or anonymized according to policy and a tombstone is written where needed to preserve audit chain integrity And a dry-run mode can be executed to report counts by jurisdiction, data type, and action (purge/anonymize/skip) without making changes And the job logs a signed summary report with counts, durations, and errors and completes at a rate >= 100k records/hour And records on legal hold are skipped with reason captured in the report And any change to retention config is versioned and audited
Legal Hold Placement and Enforcement
Given an authorized compliance user places a legal hold on a subject (volunteer ID), partner, or case ID with reason and approver When retention jobs or manual/bulk deletions execute Then any data linked to the hold (accommodations, consent, audit entries) is excluded from purge/anonymize until the hold is released And holds can be listed, filtered, and exported with fields: scope, reason, requester, approver, created_at, expires_at (optional) And releasing a hold triggers a backfill purge on the next scheduled run for all eligible records And all hold actions (create/update/release) are audited
Scoped Bulk Deletion Tool for Admins
Given an admin selects partner(s), jurisdiction(s), date range, and data types (accommodations, consent, audit logs) When they execute a bulk purge Then the tool presents a preview with exact counts and sample IDs, requires typed confirmation and second-factor approval, and executes the purge And the operation is idempotent by operation_id, rate-limited to protect systems, and resumable on failure with checkpointing And a signed deletion report (operation_id, filters, counts, error list) is generated and stored, and the entire operation is fully audited And records under legal hold are skipped and counted separately
Subject Access Request (SAR) and Incident Export
Given a verified subject or authorized admin requests an export for a volunteer ID and date range When the request is submitted Then the system produces within 2 minutes a ZIP containing JSON and CSV of current accommodations, consent artifacts, and related audit entries, plus a README and schema manifest And PII of other subjects is redacted; a SHA-256 checksum manifest is included; and a single-use signed URL is provided, expiring after 24 hours And the export request, generation, and download are fully audited
Monitoring and Alerts for Unusual Access Patterns
Given monitoring is enabled with defined thresholds When an actor reads >500 accommodation/consent records in 10 minutes or accesses from two countries within 30 minutes Then an alert is sent to Slack and email within 5 minutes including actor, thresholds exceeded, sample records, and investigation link And daily anomaly detection flags actors >3 standard deviations above their 30-day baseline read rate and opens an incident ticket automatically And alert and anomaly events are recorded and retained for at least 12 months

Badge Glance

Displays clear, color-coded accommodation badges on mobile check-in and rosters, with a quick tap for short “do/don’t” notes. Captains can filter by badge (e.g., ASL, seated, low-scent) to pre-assign stations and avoid last-minute reshuffles. Everyone gets the signal they need without hunting through profiles.

Requirements

Badge Taxonomy & Customization
"As an org admin, I want to configure a clear set of accommodation badges that reflect our needs so that volunteers and captains see consistent, meaningful signals at check-in and on rosters."
Description

Allow organizations to define and manage standardized accommodation badges with names, colors, icons, and concise labels. Ship a default library (e.g., ASL, Seated, Low-scent, Wheelchair, Quiet Space, Large Print, Dietary) that can be enabled/disabled. Support org-level custom badges with color auto-contrast validation, icon selection, and 2–3 character codes for compact display. Include an optional 60–120 character guidance blurb per badge to power “do/don’t” tips. Preserve historical integrity via stable badge IDs and soft-delete, and sync configurations across mobile and web with offline caching.

Acceptance Criteria
Enable/Disable Default Badge Library
Given a newly created organization When an admin opens Badge Settings on web Then the Default Library lists badges: ASL, Seated, Low-scent, Wheelchair, Quiet Space, Large Print, Dietary with predefined name, color, icon, 2–3 character codes, and 60–120 character guidance blurbs And each default badge is Enabled by default Given an admin disables a default badge When a volunteer profile or check-in badge picker is opened Then the disabled badge is not available for new assignments And existing historical records continue to display the badge on rosters and exports Given a default badge is re-enabled When mobile clients are online Then the badge becomes available for assignment across web and mobile within 60 seconds of the change
Create Custom Badge with Validation
Given an admin creates a new badge When entering a badge name, choosing an icon, selecting a color, and entering a code Then the code must be 2–3 uppercase alphanumeric characters and unique among active badges in the organization And the Save button remains disabled until all required fields are valid Given a background color is selected When validating contrast for code text/icon against the background Then the system enforces WCAG AA contrast (≥ 4.5:1) and blocks save if failing And a passing auto-adjusted color suggestion is presented and can be applied with one click Given an optional guidance blurb is entered When saving the badge Then the blurb length must be between 60 and 120 characters inclusive Given the badge is saved When viewing the badge details Then the badge has a system-generated immutable stable ID And the new badge appears in assignment pickers within 60 seconds on mobile/web
Edit Badge Properties with Stable ID
Given an existing badge When an admin edits its name, color, icon, code, or guidance blurb Then the badge's stable ID does not change And the code remains unique among active badges And contrast validation is re-checked and must pass before saving Given changes are saved on web When viewing any historical volunteer assignment linked to this badge ID Then the linkage remains intact and records still reference the same badge ID Given changes are saved When mobile clients are online Then updated badge properties propagate to mobile within 60 seconds
Soft-Delete Badge and Preserve Historical Integrity
Given an existing badge When an admin soft-deletes the badge Then the badge status becomes Inactive/Deleted and it is hidden from all new-assignment pickers And the badge's stable ID and properties are retained in the database And historical records, rosters, and exports continue to display the badge Given a badge is soft-deleted When creating or editing a different badge with the same code Then the system permits code reuse only if no active badge has that code And the admin is warned that the code was used by a deleted badge before confirming save Given a soft-deleted badge When an admin restores it Then it becomes available for assignment again And if another active badge has the same code, the admin must resolve the conflict before restore completes
Guidance Blurb Powers Do/Don’t Tips
Given a badge with a 60–120 character guidance blurb When a captain taps the badge on mobile Badge Glance Then a tip panel opens within one tap showing the full blurb text And the panel is screen-reader accessible and dismissible Given a badge without a blurb When tapped on Badge Glance Then the UI displays "No tips provided" and no placeholder text exceeds two lines
Sync and Offline Caching Across Web and Mobile
Given badge configurations exist on web When mobile clients launch or return to foreground with connectivity Then they fetch the latest configuration version within 60 seconds and cache it locally Given a mobile device is offline When a check-in session starts Then the last cached badge configurations (names, colors, icons, codes, blurbs) are available for display and assignment And assignments made offline reference badge stable IDs and queue for sync Given the device reconnects When the background sync runs Then queued assignments successfully post using stable IDs And local cache updates to the newest configuration version without losing offline assignments
Compact Code Display on Check-in and Rosters
Given badges assigned to a volunteer When viewing mobile check-in or roster list Then each badge displays its 2–3 character code with the configured color and icon in a compact chip no wider than 56px And text/icon contrast meets WCAG AA against the chip background Given multiple badges on one volunteer When rendering the list row Then up to 6 badge chips are shown before overflow, after which a "+N" indicator appears Given a badge code is edited When viewing check-in or rosters after sync Then the displayed code updates to the new value while the badge color and icon remain consistent
Volunteer Accommodation Capture & Consent
"As a volunteer, I want to share relevant accommodations with control over who sees them so that I can participate comfortably without repeating myself."
Description

Collect volunteer accommodation preferences during signup, profile updates, and event RSVPs, including event-specific overrides. Capture explicit consent for displaying badges to captains and check-in staff with scope options (org-wide vs event team). Validate selections against the badge taxonomy, restrict free text to short “do/don’t” notes with profanity filtering, and allow expiration dates. Provide privacy settings, access logs, and data retention controls. Implement mobile-first flows with WCAG-compliant UI, accessible labels, and localization-ready text.

Acceptance Criteria
Mobile Signup: Capture Accommodations With Explicit Consent and Scope
Given a new volunteer completes mobile signup When they reach the Accommodations step Then only badges from the active taxonomy are presented for selection And consent text is displayed with two scope options: "Org-wide" and "This event team only" And consent is optional; if unchecked, submission proceeds and accommodations are saved but will not be visible to staff UIs or exports And if consent is given, a scope selection is required and stored with timestamp and consent text version And saved selections and consent state persist and are retrievable via API and UI
Event RSVP: Event-Specific Accommodations Override
Given a volunteer has profile-level accommodations When they RSVP to an event Then they can set event-specific accommodations that override profile selections for that event only And the UI clearly labels overrides and shows the inherited profile values And on rosters and check-in, the event override is used if consent scope permits display to the event team And after the event end time, the override is archived and profile defaults resume without change
Badge Taxonomy Validation and Versioning
Given the system exposes a badge taxonomy with unique IDs and active/deprecated statuses When the client submits accommodations selections Then only active taxonomy IDs are accepted; invalid or deprecated IDs return HTTP 422 with error code BADGE_INVALID And up to 20 badges can be selected per volunteer per context (profile or event override) And when a badge becomes deprecated with a mapped replacement, existing selections are auto-migrated and the migration is recorded in the audit log
Do/Don’t Notes Constraints and Profanity Filtering
Given optional short "Do" and "Don’t" notes are available alongside badge selections When the volunteer enters notes Then each note allows up to 120 UTF-8 characters, trims surrounding whitespace, and rejects submissions containing URLs And submissions containing words from the profanity blocklist are rejected with an inline error and error code NOTE_PROFANITY And accepted notes preserve emoji and diacritics and are stored with the associated context (profile or event)
Accommodations Expiration Dates
Given a volunteer sets accommodations at profile or event level When they optionally set an expiration date (UTC) Then the date cannot be in the past and must be valid per ISO 8601 And upon reaching the expiration date at 00:00 UTC, the accommodations and notes are excluded from staff-facing views, filters, and exports And the volunteer sees the item labeled "Expired" with an option to renew or delete And an audit log entry records the expiration event
Privacy Controls, Access Logs, and Data Retention
Given accommodations and consent may be viewed by staff When a staff user with appropriate role opens a roster, check-in, or export that includes accommodations Then an access log entry is written within 2 seconds including timestamp (UTC), accessor user ID, accessor role, and context (event ID or org-wide) And volunteers can view the last 50 access entries for their data in their profile And org admins can set a retention period between 6 and 36 months (default 24) after which accommodations and notes are hard-deleted And when a volunteer revokes consent, accommodations cease to display in staff UIs and future exports within 60 seconds while preserving historical access logs with anonymized accessor identifiers
Mobile-First Accessibility and Localization Readiness
Given the accommodations and consent flows are used primarily on mobile When rendered on a 320px-wide screen Then all interactive elements have touch targets >= 44x44 dp with no horizontal scrolling required for primary actions And all fields have programmatically associated labels and hint text, and text contrast meets WCAG 2.2 AA (>= 4.5:1) And keyboard-only and screen reader navigation follows logical order with visible focus indicators and ARIA-live error announcements And all strings are externalized to localization keys with English and es-ES provided; switching device language swaps strings without code changes
Badge Glance Display on Check-in & Rosters
"As a check-in volunteer, I want to see clear badges next to each name and quickly open their do/don’t notes so that I can support them appropriately without digging into profiles."
Description

Render color-coded badge chips next to each person on mobile check-in and roster views with high contrast, scalable sizes, and accessible labels. Display up to four badges with an overflow indicator (+N). A single tap opens a microcard with concise “do/don’t” notes; a long-press reveals the full profile when permitted. Ensure performant list virtualization for rosters up to 500 entries, offline readiness with cache invalidation on reconnect, and redaction of non-consented badges. Provide localization-ready labels and theming hooks.

Acceptance Criteria
Render Badges with Overflow Indicator
Given the mobile user views the Check-in or Roster screen, when a person's row renders, then up to four permitted badge chips for that person are displayed next to their name. Given a person has zero permitted badges, when their row renders, then no badge chips or overflow indicator are shown. Given a person has one to four permitted badges, when their row renders, then exactly those badges display as color-coded chips with accessible labels. Given a person has five or more permitted badges, when their row renders, then four permitted badges display as chips and a "+N" overflow chip appears where N equals total permitted badges minus four. Given the server provides an ordered list of badges, when rendering, then the four displayed badges respect the provided order. Given the "+N" overflow chip is present, when N changes due to a data update, then the displayed N updates accordingly without a full list reload.
Tap Opens Microcard
Given a badge chip or "+N" overflow chip is visible in a person's row, when the user taps it, then a microcard overlay opens within 200 milliseconds of tap release. Given the microcard opens, when content loads, then it lists all permitted badges for that person, including those not displayed in the row. Given a badge in the microcard has associated do/don't notes, when displayed, then the notes are concise (<=200 characters) and trimmed with an ellipsis if longer. Given the microcard is open, when the user taps outside the microcard, presses Back, or swipes down, then the microcard closes and focus returns to the originating row. Given the microcard opens from either Check-in or Roster screen, then behavior and content are identical.
Long-Press Profile Access (Permission-Aware)
Given a user has permission to view full profiles, when they perform a long-press (>=500 ms) on a person's row or badge chip, then the app navigates to that person's full profile. Given a user lacks permission to view full profiles, when they perform a long-press on a person's row or badge chip, then the app does not navigate and instead shows a non-blocking notice indicating insufficient permissions. Given multiple gestures are possible, when a long-press is recognized, then a short tap action is not triggered. Given a long-press opens a profile, when navigation completes, then the originating screen state (including scroll position) is preserved for back navigation.
Accessible, Localizable, Themeable Badge Chips
Given badge chips render, then chip text and icons meet WCAG 2.1 AA contrast: text contrast ratio >= 4.5:1; non-text/icon contrast >= 3:1 in both light and dark themes. Given system font scaling from 80% to 200% is applied, when chips render, then chip text scales and chips wrap or truncate gracefully without overlap; screen readers announce the full badge name. Given a user uses a screen reader, when a badge chip receives focus, then it announces a localized accessible name including the badge name and the word "badge". Given a user attempts to tap a badge chip, then the interactive target area is at least 44 by 44 points. Given the app locale is changed (including RTL locales), when chips and microcard render, then all badge names and notes are localized and layouts mirror correctly. Given the app theme is switched, when chips render, then chip colors and typography are driven by theme tokens and update immediately without restart.
Roster Virtualization Performance (500 Entries)
Given a roster containing 500 people, when the list renders on a mid-tier device (e.g., Android 10 with 4 GB RAM or iPhone 11), then time-to-first-interactive is <= 1000 ms. Given the user scrolls continuously through the 500-entry list, then the 95th percentile frame time is <= 16.7 ms (>=60 FPS) with no visible jank > 100 ms. Given the user scrolls, then only visible rows plus a small buffer are mounted; offscreen rows are recycled/unmounted to keep memory usage stable. Given badge data updates for offscreen rows, when those rows scroll into view, then updated badges render without blocking the main thread.
Offline Readiness and Cache Invalidation on Reconnect
Given the device is offline, when the user opens Check-in or Roster, then previously cached roster entries and their permitted badges display without errors. Given the device is offline, when the user taps a badge to open the microcard, then the microcard opens and shows the last-synced notes for that person's badges. Given connectivity is restored, when the app detects reconnection, then stale badge data is invalidated and refreshed; visible rows update within 5 seconds without a full-screen reload. Given a pull-to-refresh is performed after reconnection, then roster and badge data are re-fetched and the UI reflects the latest server data. Given badge consent settings changed while offline, when reconnection occurs, then badges are re-evaluated against current consent flags before rendering.
Redaction of Non-Consented Badges
Given a person's badge is marked as non-consented for display, when their row renders, then that badge is omitted from chips and from the microcard. Given a person has only non-consented badges, when their row renders, then no badge chips or overflow indicator are shown. Given badge payloads include consent/redaction flags, when the client receives data, then any badge with show=false or redacted=true is filtered out before rendering or caching for offline use. Given accessibility services are active, when rendering redacted content, then no redacted badge names or notes are exposed in the accessibility tree.
Badge-based Filtering & Station Pre-assignment
"As a captain, I want to filter by accommodations and get station suggestions so that I can place volunteers appropriately before check-in and reduce last-minute changes."
Description

Enable captains to filter rosters by one or more badges and intersect results with roles/stations and shift times. Allow stations to declare required or avoid badges (e.g., requires ASL; avoid strong scents). Generate pre-assignment suggestions that match volunteers to stations, flag conflicts, and minimize reshuffles. Integrate with the existing shift assignment engine, persist assignments, and sync in real time across team devices with optimistic updates and conflict resolution.

Acceptance Criteria
Filter roster by multiple badges within a shift window
Given a captain is viewing the roster for Shift S When the captain selects badges "ASL" and "Low-Scent" and applies the filter Then only volunteers who have both badges are shown And only volunteers available during Shift S’s time range are shown And the visible result count matches the number of returned volunteers And a Clear Filters control resets the roster to its unfiltered state
Station rules: required and avoid badges
Given Station "Check-In" for Shift S has Required badges [ASL] and Avoid badges [Strong Scent] When generating suggestions or attempting a manual assignment Then only volunteers with ASL are eligible for auto-suggestion And volunteers with the Strong Scent badge are excluded from auto-suggestion And manual selection of an ineligible volunteer is flagged with reasons (e.g., "Missing: ASL", "Avoid: Strong Scent")
Pre-assignment suggestion generation matches constraints
Given station capacities, role qualifications, and station badge rules are configured for Shift S When the captain taps "Generate Suggestions" Then suggested assignments satisfy all Required and Avoid badge rules And respect volunteer role qualifications and availability overlap with Shift S And do not assign any volunteer to overlapping stations And fill each station up to capacity where eligible volunteers exist And the suggestion run completes within 3 seconds for rosters up to 300 volunteers
Conflict flagging and override for manual assignments
Given a captain selects a volunteer who violates a station rule or has an overlapping assignment When the captain attempts to save the assignment Then the system displays a conflict banner listing each conflict reason And prevents save unless the captain chooses Override and provides a reason And overridden assignments persist with an audit record (captain, timestamp, reasons)
Real-time sync and optimistic updates across devices
Given Captain A assigns Volunteer V to Station X for Shift S When Captain A saves the assignment Then the assignment appears immediately on A’s device (optimistic) and is persisted to the server And within 2 seconds the same assignment is visible on Captain B’s device And the assignment remains after app restart on both devices
Concurrent edit conflict resolution
Given Captain A and Captain B concurrently assign Volunteer V to different overlapping stations for Shift S When the server processes both assignments Then the first-commit assignment is accepted and the later conflicting assignment is rejected And the rejecting client receives a conflict message identifying the volunteer, conflicting station, and time window And the UI offers a one-tap Re-suggest action to propose eligible alternatives
Integration and persistence with existing assignment engine
Given the captain accepts suggested pre-assignments for Shift S When they confirm Save Then assignments are created in the existing shift assignment engine And they appear in roster, station, and check-in views as committed assignments And each assignment can be undone individually or via "Revert Suggestions" without orphaning related records
Do/Don’t Notes Templates & Editing
"As a volunteer, I want short, respectful do/don’t notes associated with my badges so that staff know how to support me without oversharing."
Description

Provide structured, concise “do” and “don’t” notes linked to each badge, with organization-configurable templates and per-volunteer overrides. Limit entries to 140 characters per side with smart truncation and empathetic language guidance. Enforce permissions so volunteers edit their own notes while captains add event-specific notes; track versions with an audit trail. Surface these notes in microcards, printable rosters, and exports; apply a prohibited terms list and spell-check to maintain respectful phrasing.

Acceptance Criteria
Org admin configures badge note templates
Given an org admin is on Badge Settings > Notes Templates for a specific badge When they create or edit the Do and Don't templates and click Save Then each template is limited to 140 characters and a live counter updates as they type And if text exceeds 140, a smart truncation preview shows an ellipsis without cutting a word and Save is blocked until within limit And empathetic language guidance appears when non-inclusive phrasing is detected with suggested rewrites And the change persists and applies by default to volunteers without a personal override for that badge And an audit record is created capturing who made the change, timestamp, badge, and before/after values
Volunteer edits own badge notes with guidance
Given a logged-in volunteer viewing their Profile > Accommodations for a specific badge When they edit Do and Don't notes and attempt to Save Then each side is limited to 140 characters with a live counter and smart truncation preview And prohibited terms are highlighted inline and Save is disabled until all flagged terms are resolved And spell-check underlines suspected misspellings and offers corrections And empathetic language tips are shown non-blocking while editing And changes save successfully and are attributed in the audit trail to the volunteer with timestamp and version increment
Captain adds event-specific notes for a volunteer
Given a captain on the Event Roster for Event X viewing Volunteer Y with Badge Z When they add or edit event-specific Do/Don't notes and Save Then the event-specific notes are stored at the Event X scope and do not modify Volunteer Y's permanent notes And Event X microcards and rosters display event-specific notes first with an Event label, followed by permanent notes And an audit entry records the captain, event, volunteer, badge, and before/after values
Role-based permissions enforced
Given role permissions for Volunteer, Captain, and Admin When a user attempts to edit Do/Don't notes Then a Volunteer can edit only their own permanent notes and cannot edit others' And a Captain can add/edit event-specific notes only and cannot edit permanent notes unless explicitly granted And an Admin can edit templates and any permanent notes And unauthorized actions are blocked with a 403 and friendly message, no changes are persisted, and the attempt is logged in the audit trail
Notes surface across microcards, printable rosters, and exports
Given notes exist (permanent and/or event-specific) for a volunteer's badge When viewing mobile check-in microcards, printing rosters, or exporting CSV Then microcards show a one-tap Do/Don't reveal with smart truncation (whole-word ellipsis) and badge color-coding And printable rosters include Do and Don't columns with event-specific notes labeled; lines wrap without breaking words And CSV exports include columns do_notes_permanent, dont_notes_permanent, do_notes_event, dont_notes_event in UTF-8 plain text without markup And prohibited terms never appear because saves with prohibited terms are blocked
Prohibited terms list management and enforcement
Given an org admin manages the prohibited terms list When they add, edit, or remove terms and Save Then the list updates immediately and applies case-insensitively, matching whole words and common inflections And when any user types a prohibited term in notes, the term is highlighted with an inline message and Save is disabled until rephrased And the last editor and timestamp of the prohibited terms list are recorded in the audit trail
Version history and audit trail visibility
Given a volunteer's badge notes have multiple edits over time When an admin opens the Notes History for that volunteer and badge Then a chronological list of versions is shown with editor, timestamp, scope (template/permanent/event), and before/after diffs And viewing history is restricted to Admin role; other roles cannot access it And the history is retained for at least 24 months and can be filtered by date range and editor
Permissions, Privacy, and Audit
"As an admin, I want clear permissions and audit trails around accommodations so that we protect volunteer privacy while enabling teams to act on the information."
Description

Implement role-based access controls for viewing and editing accommodation badges and notes across Volunteer, Captain, and Admin roles. Honor consent scopes, restrict sensitive data from general volunteers, and provide a quick privacy toggle for shared displays. Log all views and edits of accommodation data with timestamp, actor, and context, and expose downloadable audit reports. Ensure encryption in transit and at rest, data export on request, and alignment with GiveCrew’s privacy policy.

Acceptance Criteria
RBAC: Badge and Notes Access by Role
Given I am authenticated as Volunteer, When I open check-in or roster, Then I see only badge icons explicitly consented for Peers visibility and I do not see any notes or edit controls. Given I am authenticated as Captain, When I open check-in or roster, Then I can view badge icons and short do/don't notes for volunteers whose consent scope includes Captains, and I cannot edit unless granted the Edit Accommodations permission. Given I am authenticated as Admin, When I open any volunteer profile or roster, Then I can view and edit all badges and notes. Given any role without permission, When I attempt direct URL or API access to restricted accommodation data, Then I receive 403 Forbidden and the attempt is logged with actor, timestamp, and context.
Consent Scopes: Visibility and Editing Restrictions
Given a volunteer sets their accommodation visibility to Admin+Captain, When rosters and check-in screens load, Then only Admins and Captains see badges and notes; Volunteers do not. Given visibility is Peers, When rosters and check-in screens load, Then Volunteers see badge icons only; notes remain hidden from Volunteers. Given consent is not provided, When accommodation data exists, Then default visibility is Admin+Captain. Given a volunteer updates their consent scope, When the next data fetch occurs (<=60 seconds), Then visibility updates across roster, check-in, and profile views. Given consent scope is Admin Only, When a Captain attempts to view notes, Then the note content is masked and a Restricted by consent indicator shows.
Shared Display Privacy Toggle Behavior
Given I am on the mobile check-in or roster screen, When I enable Privacy Mode, Then all accommodation notes are hidden, badge labels are replaced with generic icons, and quick-tap note previews are disabled. Given Privacy Mode is enabled, When a Captain filters by badge, Then filtering works without revealing hidden note content. Given I am a Captain with permission, When I temporarily disable Privacy Mode, Then unmasked details are visible only on my device for up to 5 minutes of inactivity or until I re-enable, after which Privacy Mode auto-reengages. Given any user toggles Privacy Mode, When the toggle state changes, Then the event is logged without capturing note content.
Audit Logs: Views, Edits, and Downloadable Reports
Given any user views accommodation data, When the view occurs, Then an audit entry records actor ID, role, volunteer ID, action view, screen or context, and UTC timestamp. Given any user edits badges or notes, When the edit is saved, Then an audit entry records actor ID, role, volunteer ID, action edit, fields changed with before and after hashes, and UTC timestamp. Given I am an Admin, When I request an audit report for a date range with optional volunteer filter, Then a CSV is generated within 60 seconds and available for download with the expected columns and row count. Given audit logs exist, When I download the report, Then entries are immutable and a checksum verifies file integrity.
Security: Encryption In Transit and At Rest
Given any client connects to the API, When using HTTP, Then the request is redirected to HTTPS and HSTS is enforced. Given a TLS handshake, When using protocols below TLS 1.2 or weak ciphers, Then the connection is rejected. Given data is stored in the database or backups, When inspected via infrastructure configuration, Then storage encryption at rest is enabled using AES-256 or provider-managed keys. Given mobile or web traffic, When intercepted via a proxy without valid certificates, Then the app refuses the connection due to certificate validation failure.
Data Export: Accommodation Data on Request
Given I am an Admin, When I request a volunteer's accommodation data export, Then the system produces a downloadable package (JSON and CSV) within 5 minutes containing badges, notes, consent scopes, and related audit entries. Given an export is generated, When the download link is delivered, Then it is accessible in-app and via email, requires authentication, and expires after 24 hours. Given an Admin without View Notes permission requests export, When the package is created, Then sensitive note content is redacted and a reason is included. Given an export request is submitted, When processing completes, Then the request and fulfillment are recorded in the audit log.
Privacy Policy Alignment at Collection Points
Given a user attempts to add or edit accommodation data, When the current privacy policy version has not been accepted, Then the user is shown the policy and must accept before proceeding; the accepted version and timestamp are recorded. Given the privacy policy is updated, When users next access accommodation features, Then they are prompted to accept the new version and are blocked from creating or viewing notes until acceptance. Given policy access is required, When on check-in and roster screens, Then a visible Privacy link opens the current policy without leaving the workflow.
Analytics & Impact Board Integration
"As a program lead, I want to see how Badge Glance affects operations and volunteer experience so that we can quantify impact and iterate."
Description

Instrument Badge Glance interactions (badge views, filters applied, pre-assignments accepted) and operational outcomes (reshuffle reductions, show-up rate deltas for accommodated volunteers). Aggregate metrics in a privacy-preserving way and surface them on the Impact Board and in weekly summaries. Provide insights to refine badge taxonomy and note templates, and allow exports for grant reporting.

Acceptance Criteria
Badge Interaction Telemetry Captured
Given a captain views a roster on mobile When they tap a volunteer and the Badge Glance panel opens Then exactly one badge_view event with schema v1.0 is recorded with fields: event_name, org_id, actor_role, timestamp_utc, badge_ids[] And if the device is offline, the event is queued locally and transmitted within 120 seconds of reconnection And duplicate taps within 1 second do not create duplicate events And applying a badge filter records one filter_applied event including selected_badge_ids[] and result_count And accepting a pre-assignment from a badge filter records one preassignment_accepted event with station_id and shift_id And all payloads exclude free-text notes and volunteer personal identifiers
Privacy-Preserving Aggregation Rules Enforced
Given aggregated metrics are generated for Badge Glance When a cohort for a metric has fewer than 10 unique volunteers in the selected period Then the metric is suppressed and the UI displays "Insufficient data" And all exported counts are rounded to the nearest 5 And time buckets are day-level or coarser; no timestamps finer than day are exposed And free-text note content is excluded; only template usage counts are aggregated And no volunteer identifiers, emails, or phone numbers appear in the Impact Board, weekly summaries, or exports
Impact Board Displays Badge Glance Metrics
Given an org admin opens the Impact Board When the date range is set to Last 30 Days Then cards display Badge Views, Filters Applied, and Pre-assignments Accepted with counts that meet privacy thresholds And Operational Outcomes show Reshuffle Rate (reassignments within 2 hours of shift start divided by total shifts) and Show-up Delta for accommodated volunteers (show_up_rate_accommodated_preassigned minus show_up_rate_all_others) And metrics are refreshed within 2 hours of new events arriving And percent change vs the previous comparable period is displayed for each metric And any suppressed metric shows a standard "Insufficient data" message instead of a number
Weekly Summary Includes Badge Glance Insights
Given it is Monday 09:00 in the org’s local time When weekly summaries are generated Then admins and captains with Reports permission receive an email and in-app summary And the summary includes last 7-day counts for Badge Views, Filters Applied, Pre-assignments Accepted, Reshuffle Rate, Show-up Delta, and the top 3 badges by impact And numbers match the Impact Board for the same window at send time And the summary contains no personal identifiers and honors user notification opt-out settings And links in the summary open the Impact Board with the same date range applied
Insights to Refine Badges and Note Templates
Given at least 90 days of Badge Glance data and privacy thresholds are met When Insights are viewed Then up to 5 recommendations are listed with rationale and supporting metrics (e.g., Archive, Clarify Label, Promote Template) And Archive candidates are badges used in <1% of assignments and with <50 total uses in the window And Clarify candidates have pre-assignment acceptance rates >20% below median or note template edit rates >30% And selecting a recommendation opens the taxonomy editor with the targeted badge/template preselected And applying a change records a settings_change event
Grant Reporting Export
Given a user with Reports permission opens Exports When they request a Badge Glance Impact export for a date range up to 12 months Then CSV and PDF files generate within 30 seconds And files include metric definitions, date range, org name, and aggregated counts/rates that meet privacy thresholds And columns include: badge_views, filters_applied, preassignments_accepted, reshuffle_rate, show_up_delta, top_badges_by_impact And free-text content and any identifiers are excluded from the files And exported values match the Impact Board for the same window within 2%

Captain Cues

Turns each accommodation response into a tiny, role-aware checklist (e.g., reserve front-row seat, mark scent-free zone, pair with ASL interpreter). One-tap ‘Done’ updates the roster, and gentle nudges surface unresolved items before call time. Captains operate inclusively without juggling mental to-dos.

Requirements

Accommodation Intake Normalization
"As an event captain, I want accommodation responses normalized into clear, actionable items so that I can quickly see what needs to be prepared without combing through free-text notes."
Description

Transform free-text and checkbox accommodation responses from signups, RSVPs, and volunteer profiles into structured, canonical records linked to the person and the specific event/shift. Support a standard taxonomy (e.g., mobility, sensory, communication), custom entries, severity/lead-time tagging, and de-duplication across forms. Store an AccommodationRequest entity with scope (global vs. event-specific), consent flags, and default persistence rules. Expose normalized data to Captain Cues for downstream cue generation while honoring visibility permissions. Provide admin settings to map form fields to canonical attributes and migration scripts to backfill existing data.

Acceptance Criteria
Normalize Free-Text and Checkbox Inputs into Canonical AccommodationRequest
Given a signup includes checkbox "Needs wheelchair access"=true and free-text "Prefer low scent area" with admin mappings to mobility.wheelchair_access and sensory.scent_free_zone When normalization runs Then two canonical AccommodationRequest records are created with canonicalAttributes mobility.wheelchair_access and sensory.scent_free_zone, isCustom=false, and sourceFormId set to the signup form id Given a signup includes free-text "Needs quiet room to decompress" with no mapping When normalization runs Then one AccommodationRequest record is created with canonicalAttribute=custom.other, isCustom=true, and originalText preserved Given an admin mapping references an unknown taxonomy key When normalization runs Then the intake is rejected with error code ACCTAXONOMY_001 and no AccommodationRequest records are created Given a batch of 100 signups When normalization runs Then the 95th percentile processing time per record is <= 500 ms
Scope and Persistence: Global vs Event-Specific with Consent
Given a volunteer profile submission includes "Needs ASL interpreter" and "Save for future events" checked When the submission is saved Then a global-scoped AccommodationRequest is created/updated with scope=global and consentPersist=true and no eventId set Given an RSVP for Event A includes "Quiet space" and "Save for future events" unchecked When the RSVP is saved Then an event-scoped AccommodationRequest is created with scope=event and eventId=Event A and no global record is created Given a volunteer revokes consent for persistence When the change is saved Then any global-scoped AccommodationRequest for that person is archived (archivedAt set) and excluded from downstream exposure
Visibility and Permissioned Exposure to Captain Cues
Given a Captain assigned to Event A requests accommodations via the Captain Cues endpoint for Event A When the request is authorized Then only accommodations for volunteers assigned to Event A are returned with fields [personId, shiftId, canonicalAttribute, severity, leadTimeHours, needsBy, operationalNotes] and fields flagged privacyLevel=private are redacted Given a user without Captain or OrganizerAdmin role requests the same endpoint When the request is processed Then the API responds 403 Forbidden and no data is returned Given a Captain for Event A requests accommodations for Event B When the request is processed Then the API responds 403 Forbidden Given any successful access When the response is produced Then an audit log entry is written with accessorRole, accessorId, eventId, timestamp, and recordCount
Severity and Lead-Time Tagging with Needs-By Calculation
Given an accommodation has severity=high and leadTimeHours=72 and the event start is 2025-09-15T18:00:00Z When the record is saved Then needsBy is set to 2025-09-12T18:00:00Z with no rounding Given an accommodation omits leadTimeHours but the taxonomy default for its canonicalAttribute is 24 When the record is saved Then leadTimeHours is set to 24 and needsBy is computed accordingly Given an accommodation has severity=extreme (not in [low, medium, high, critical]) When validation runs Then the save is rejected with error code ACCVALID_001 and the record is not created/updated Given an accommodation has leadTimeHours=-1 When validation runs Then the save is rejected with error code ACCVALID_002 and the record is not created/updated
Cross-Form De-duplication and Merge Rules
Given the same person submits "ASL interpreter" via signup and later via RSVP for Event A When normalization runs Then exactly one event-scoped AccommodationRequest exists for (personId, Event A, canonicalAttribute=communication.asl_interpreter) and its sources include both submission ids and lastUpdatedAt equals the latest submission timestamp Given a person has a global "ASL interpreter" accommodation and later submits an Event A-specific "ASL interpreter" with severity=critical When merge runs Then the Event A record severity=critical and the global record remains unchanged Given identical accommodations are re-submitted with identical payload for the same scope When normalization runs Then no new records are created and the operation is idempotent Given duplicates are detected across signup, RSVP, and profile for the same (personId, scopeKey, canonicalAttribute) When merge completes Then the system maintains a single record and merges sources without data loss
Admin Mapping of Form Fields to Canonical Taxonomy
Given an OrganizerAdmin opens Admin > Field Mapping and maps form field "Wheelchair access" to mobility.wheelchair_access with synonym "wheel chair" When Save is clicked Then the mapping is stored as a new version and returns 200 OK and the active mapping version increments by 1 Given the admin enters a sample payload in Preview When Preview is run Then the normalized output displays the expected canonicalAttribute and flags isCustom correctly Given the admin rolls back to the prior version When Rollback is confirmed Then the prior version becomes active and a rollback audit entry is recorded Given a new mapping version is activated When new signups are ingested Then only new records use the new mapping and previously normalized records remain unchanged
Migration Backfill of Existing Accommodation Data
Given historical signup, RSVP, and profile tables contain accommodation data When the migration script runs in dry-run mode Then it outputs a report with counts of prospective AccommodationRequests, deduplications, and validation errors without writing to the database Given the migration is executed in apply mode When it completes Then AccommodationRequest records are created with correct personId/eventId links, consent flags set from historical consent columns, duplicates merged per (personId, scopeKey, canonicalAttribute), and an audit summary is stored Given the migration is re-run after a successful apply When it completes Then no duplicate AccommodationRequest records are created (idempotent) and the summary indicates 0 inserts and 0 merges Given a dataset of 50,000 historical rows When migration runs in apply mode Then it completes within 30 minutes and with zero unhandled exceptions
Role-Aware Cue Templates
"As a shift captain, I want accommodation cues tailored to each role so that volunteers get the right prep steps for their assignments."
Description

Generate concise, role-specific checklists from each accommodation using a template library that maps accommodation categories to event roles (e.g., Greeter, Registration, Stage). Include defaults (e.g., reserve front-row seat, mark scent-free zone) with due times relative to call time and the ability to add local overrides per event. Recompute cues when roles or assignments change and when accommodations are added/removed. Support localization, custom labels, and toggleable templates per organization. Provide a preview mode for captains to review and edit generated cues before they are assigned.

Acceptance Criteria
Template Mapping Generates Role-Specific Cues
Given an event has roles configured and an accommodation with a mapped category exists When cue generation runs Then cues are created only for roles mapped to that accommodation category in the template library And each generated cue is assigned to the correct role with a unique cue ID And roles without a mapping receive no cues for that accommodation
Default Tasks and Due Times Relative to Call Time
Given an event call time T and a template with tasks that include relative due offsets (e.g., -60m, -10m, +15m) When cue generation runs Then each generated cue has an absolute due time computed as T + offset in the event’s timezone And default tasks (e.g., reserve front-row seat, mark scent-free zone) are included And computed due times are accurate within 1 minute
Event-Level Template Overrides
Given an organizer creates local overrides for an event (add/edit/remove tasks or change offsets/labels) When cues are generated or regenerated for that event Then the overrides apply only to that event and do not modify the organization’s template library And removed tasks do not generate cues And edited tasks update their labels and due offsets in generated cues
Automatic Recompute on Roster and Accommodation Changes
Given cues have been generated for an event When a role assignment changes, a role is added/removed, or an accommodation is added/removed/updated Then cue lists are recomputed within 5 seconds And cues reflect the current role assignments and accommodations And duplicate cues are not created And cues tied to removed accommodations are archived and hidden from the active list And manual completion status and captain notes persist when the underlying cue remains equivalent
Localization and Custom Labels
Given an organization sets locale and custom role labels When cues are generated and viewed Then cue text from templates is localized to the selected locale And date/time formats follow the locale And custom role labels appear everywhere role names are shown And English fallback is used for any missing translation keys
Template Toggle Controls per Organization
Given a template category is toggled off for an organization When cues are generated for events under that organization Then no cues are created from the disabled template category And previously generated cues remain unchanged for existing events And re-enabling the template restores cue generation for subsequent events
Captain Preview and Edit Before Assignment
Given a captain opens Preview Mode for an upcoming event When generated cues are displayed Then the captain can approve, edit labels, adjust due offsets, re-order, or delete cues before assignment And saving commits the modified cues to the event and assigns them to roles And canceling discards changes and leaves existing assignments untouched And the preview shows counts of cues per role and per accommodation
One-Tap Cue Completion & Roster Sync
"As a captain on-site, I want to mark a cue done with one tap so that the roster and everyone else are instantly updated."
Description

Provide a mobile-first checklist UI with large tap targets for quick, one-tap completion of cues. On completion, instantly sync the roster and related resources (e.g., seating map, interpreter pairing, transport pickup) and emit updates to all connected clients. Record audit logs (who, when, where) and attach notes or photos as proof if needed. Handle offline mode with queued actions and conflict resolution. Enforce permissions so only authorized users can complete or revert cues. Trigger follow-on automations such as sending confirmations, updating the Impact Board, or clearing pending nudges.

Acceptance Criteria
One-Tap Cue Completion Updates Roster
Given a captain is viewing the mobile checklist for an active event When the captain taps Done on a cue Then the cue state toggles to Completed locally within 300 ms And the roster entry and linked resources (seating map, interpreter pairing, transport pickup) are updated on the server within 2 seconds And a success confirmation is displayed without navigating away And the update is persisted and visible on re-open of the checklist
Real-Time Client Update Broadcast
Given at least three clients (mobile/web) are connected to the same event When one client completes or reverts a cue Then all connected clients receive a push update and reflect the new cue state within 2 seconds without manual refresh And no duplicate updates are rendered for the same cue action And clients that reconnect within 5 minutes receive the missed updates via delta sync within 3 seconds of reconnect
Audit Log and Proof Attachment
Given an authorized user completes or reverts a cue When the action is committed Then an audit record is stored with user ID, role, action (complete/revert), cue ID, UTC timestamp, and source (device/app version/IP), plus geo-coordinates if permissioned And the user can optionally attach up to 3 photos (<=10 MB each) and a note (<=1000 chars) before confirming And attachments are uploaded, virus-scanned, and linked to the audit record And users with View Audit permission can see the log and attachments within 2 seconds of refresh
Offline Queue and Conflict Resolution
Given the device is offline while viewing a checklist When the user taps Done on a cue Then the cue is marked Pending locally and queued for sync with a visible pending badge And the action auto-syncs within 5 seconds of reconnection And if a conflict exists (cue changed on server), then the server version wins, the local action is recorded in audit as conflicted, and the user sees a non-blocking conflict banner with a View Details option And no data (notes/photos) is lost; queued attachments upload automatically on reconnect
Permission Enforcement for Cue Actions
Given role-based permissions are configured for the event When an unauthorized user attempts to complete or revert a cue Then the action is blocked, a concise error is shown, and the attempt is logged with user ID and timestamp And when an authorized user performs the same action Then the action succeeds and is logged And permission changes take effect on clients within 60 seconds or on next refresh
Follow-On Automations on Completion
Given automations are enabled for the event When a cue is completed Then confirmation messages (e.g., SMS/email) are sent to affected parties within 30 seconds And the Impact Board metrics update within 10 seconds And any pending nudges for that cue are cleared immediately And automation results (success/failure per task) are recorded and surfaced in an activity panel; failures retry up to 3 times with exponential backoff
Mobile-First Accessibility and Tap Targets
Given the checklist is rendered on a mobile device When the user interacts with cue items Then primary tap targets are at least 44pt (iOS) / 48dp (Android) And the UI is navigable via VoiceOver/TalkBack with meaningful labels for cues and actions And color contrast meets WCAG 2.2 AA and focus states are visible And haptic feedback confirms one-tap completion And the checklist loads in under 2 seconds on a 3G connection for events with up to 100 cues
Pre-Call Nudge Scheduler
"As a coordinator, I want unresolved cues to surface before call time so that nothing slips through and the event runs inclusively."
Description

Schedule gentle reminders for unresolved cues based on call time offsets (e.g., T-24h, T-2h, T-30m) and cue criticality. Deliver nudges via in-app notifications and optional push, with quiet hours, snooze, and escalation to backup owners if the primary assignee is unresponsive. Provide a pre-call digest summarizing remaining items and auto-adjust schedules when call times or assignments shift. Track nudge effectiveness and completion rates to refine default timings. Surface an on-call banner highlighting critical outstanding cues as the event window opens.

Acceptance Criteria
T-Offset Nudge Scheduling by Criticality
Given an event has a call time and unresolved cues labeled Critical, High, or Normal When the nudge scheduler runs Then nudges are scheduled only for unresolved cues at these offsets relative to call time: - Critical: T-24h, T-2h, T-30m - High: T-24h, T-2h - Normal: T-24h And any offsets in the past at scheduling time are skipped And no nudge is scheduled after the call time
Notification Delivery, Quiet Hours, and Snooze Controls
Given an assignee’s notification preferences (in-app required, push optional) and device token status When a nudge triggers Then an in-app notification is created within 60 seconds And a push notification is sent only if the assignee has opted in and has a valid device token And the notification includes cue title, criticality, T-offset, and a deep link to the cue checklist Given org-level quiet hours are set (21:00–08:00 in the event’s time zone) When a nudge’s scheduled time falls within quiet hours Then no push notification is sent and the in-app notification is queued And when quiet hours end, if the cue remains unresolved and the event has not passed call time, the queued push is sent within 60 seconds (subject to preferences) When the assignee taps Snooze for 15m, 30m, or 1h on a nudge Then the next nudge for that cue is rescheduled by the selected interval and duplicate nudges for that cue are suppressed during the snooze window
Escalation to Backup Owner on Non-Response
Given a cue has a primary owner, a backup owner, and the latest pre-call nudge within the T-2h window has been sent to the primary When 15 minutes elapse after that nudge and the cue is still not marked Done by the primary Then an escalation nudge is sent to the backup owner via in-app (and push if enabled) And the cue displays an “Escalated” status visible to both owners with a timestamp And any future scheduled nudges for the primary are paused until the cue is completed or reassigned And if the primary completes the cue before the backup does, the escalation state is cleared and pending backup nudges are canceled
Pre-Call Digest for Captains
Given it is T-60m prior to call time or a captain manually requests a digest When the digest is generated Then it lists only unresolved cues grouped by Critical, High, and Normal with counts per group And shows owner, last nudge timestamp, and next scheduled nudge per cue And includes deep links for Mark Done, Reassign, and Snooze actions And if there are zero unresolved cues, no digest notification is sent and an “All clear” banner is shown instead
Auto-Adjust on Call Time or Ownership Changes
Given future nudge schedules exist for an event When the event’s call time is changed or a cue’s owner is reassigned Then all future nudges for affected cues are recalculated using the new call time and/or new owner’s preferences And superseded scheduled nudges are canceled without being sent, and the audit log records the original and new times And no duplicate nudges are created for the same offset after recalculation And if the new call time is within 30 minutes from now and critical cues remain unresolved, an immediate nudge is sent to current owners and the on-call banner is shown
Effectiveness Tracking and Timing Refinement
Given nudge sends and cue completion events are logged with cue id, criticality, channel, and timestamps When viewing the Nudge Effectiveness report for the last 30 days with at least 100 nudges per criticality level Then the system shows completion-within-15-min rate by channel and criticality and average time-to-complete And if an alternate tested offset yields a >=5% higher completion-within-15-min rate for a criticality with n>=100, the system generates a proposal to update default offsets for that criticality And when an admin approves the proposal, the new default offsets are versioned and applied to future schedules
On-Call Banner as Event Window Opens
Given the event enters its on-call window at T-30m before call time When a captain opens the app during this window Then a persistent on-call banner is displayed showing the count of critical unresolved cues and a prioritized list with deep links And the banner updates at least every 30 seconds as cue statuses change And the banner disappears when critical unresolved cues count reaches zero or at call time + 10 minutes, whichever comes first
Cue Ownership & Delegation
"As a captain, I want to assign specific cues to teammates so that responsibilities are clear and follow-up is easy."
Description

Allow captains to assign each cue to an owner (person or role), set optional due-by timestamps, add context notes, and reassign as needed. Support bulk assignment, @mentions, and visibility of workload across owners to balance tasks. Prevent orphaned critical cues with required ownership and highlight unassigned items. Provide filters by owner, status, and criticality, and a personal "My Cues" view for each team member. Maintain an activity trail of assignment changes for accountability.

Acceptance Criteria
Assign owner (person or role) with optional due-by and notes on cue create/edit
Given a captain creates a new cue and selects Critical criticality When they attempt to save without selecting an owner Then the save is blocked and an inline error "Owner is required for critical cues" is displayed and the Owner field is focused Given a new or existing cue When a captain selects an owner (person or role) and optionally sets a due-by timestamp and enters a context note Then the cue is saved with owner and due-by persisted and immediately reflected in the cue list Given a non-critical cue saved without an owner When viewed in any list Then it displays an "Unassigned" badge and is included in the Unassigned filter Given a cue with a due-by timestamp When displayed to any user Then the timestamp is shown in the viewer’s local timezone and the exact value is available on tap
Reassign cue owner and maintain immutable activity trail
Given a cue with an existing owner When a captain changes the owner to a different person or role Then the cue updates and an activity entry is appended recording previous owner, new owner, actor, timestamp, and optional reason note Given the activity trail for a cue When viewed by any user with access Then entries are ordered newest-first, read-only, and show full details Given a reassignment that fails due to permissions or invalid target When attempted Then no change is saved, a clear error is displayed, and no activity entry is created
Bulk assign owners, due-by, and notes across selected cues
Given a list of cues with multi-select enabled When a captain selects 2 or more cues and chooses Bulk Assign to set an owner (person or role) and optionally a due-by and note Then all selected editable cues are updated in one action and a confirmation shows the number successfully updated Given a bulk assignment where some cues are no longer editable When the update is submitted Then editable cues are updated, non-editable cues are skipped with reasons listed, and the confirmation summarizes successes and skips Given a bulk assignment completion When viewing workload by owner Then counts reflect the changes immediately
@mentions in notes trigger typeahead and notifications
Given the notes field for a cue When a user types "@" followed by 2 or more characters Then a typeahead lists matching people and roles the user can mention Given a note containing one or more @mentions When the note is saved Then each mentioned person and each member of a mentioned role receives an in-app notification linking to the cue Given a saved note with @mentions When viewed in the activity trail Then mentions render as tappable chips to the mentioned profile or role
Workload visibility by owner to balance tasks
Given the Owners panel When opened Then it displays for each owner (person or role) the count of open cues, count of critical open cues, and next due-by, sortable by any column Given an owner selected from the Owners panel When the captain assigns a new cue to that owner Then the panel updates the counts in real time and the cue's owner field reflects the selection Given two owners with different workloads When the captain opens the assignment picker Then the picker shows each owner with their open-cue count and critical count to inform balancing
Filters and My Cues view
Given the cues list When a user filters by Owner, Status, and Criticality (individually or in combination) Then only matching cues are shown and the active filters are displayed as removable chips Given a signed-in user When they open My Cues Then the list shows all open cues owned directly by the user and all open cues owned by any role the user belongs to, sorted by due-by ascending then criticality descending Given a user with no owned cues When they open My Cues Then an empty state is displayed with a link to All Cues
Prevent orphaned critical cues in workflow transitions
Given a critical cue without an owner When any user attempts to move it to Ready, In Progress, or Done Then the transition is blocked with an error "Assign an owner before proceeding" Given a critical cue with an owner When moved to Done Then the cue transitions successfully and is removed from Unassigned filters and the Unassigned badge is not shown Given a non-critical cue without an owner When moved between statuses Then the transition is allowed and the Unassigned badge persists and the cue remains in Unassigned filters
Accessibility Resource Directory & Matching
"As an organizer, I want suggestions for resources that satisfy a cue so that I can fulfill accommodations quickly and correctly."
Description

Maintain a lightweight directory of accessibility resources (e.g., ASL interpreters, captioners, wheelchair seating zones, transport partners) with capabilities, availability windows, locations, and contact methods. Auto-suggest resources for cues based on accommodation type, event location/time, and historical reliability. Provide quick-book workflows (templated emails/SMS, holds, and confirmations) and track costs/approvals when applicable. Cache preferred vendors per organization and flag lead-time-sensitive needs to prioritize early booking. Link confirmed resources back to the cue for transparency.

Acceptance Criteria
Create and Maintain Accessibility Resource Entries
Given I have Captain or Admin permissions When I add a new resource with name, type (ASL, captions, seating, transport), capability tags, service locations, availability windows (with timezone), and at least one contact method Then the system validates required fields, blocks save on missing/invalid data, and shows inline errors And when I save a valid resource Then it is persisted and searchable by name, type, capability, and location within 2 seconds And when I edit an existing resource Then changes are saved and become searchable within 2 seconds And overlapping or invalid availability windows are blocked with error messaging
Auto-Suggest Resources for Cue Based on Accommodation and Context
Given a cue specifies an accommodation type with event date/time, location, and organization context When I open the Suggestions panel on the cue Then results return within 2 seconds and include only resources matching the accommodation type and available during the event window And suggestions are ranked by a fit score weighted by availability overlap, proximity, organization preference, and reliability score And each suggestion displays capability match, availability snippet, distance, reliability (0–100), and primary contact method And resources outside their service area or marked unavailable are excluded
Quick-Book with Templated Outreach, Holds, and Confirmations
Given I select a suggested resource When I tap Quick Book Then I can choose SMS or Email templates prefilled with event details and send in one tap And the booking enters Hold status with an expiry timestamp visible on the cue And when the resource replies with a confirm keyword or I mark Confirmed, the booking status updates to Confirmed, the hold is cleared, the cue checklist item is marked Done, and the resource is linked to the cue and event roster for the event team to view And lack of response by hold expiry triggers an automatic nudge and returns the resource to Suggestions
Cost Entry, Threshold Approval, and Final Reconciliation
Given a booking includes an expected cost or rate When the expected cost exceeds the organization’s approval threshold Then an approval request is sent to the designated approver and the booking cannot be marked Confirmed until approved And approvals or denials are time-stamped and recorded on the cue and resource record And when the event completes Then I can record final cost, attach an invoice, and mark the cost as reconciled And cost totals are included in exports and cost reports
Preferred Vendors Cached Per Organization
Given an organization marks a resource as Preferred for specific accommodation types and regions When generating suggestions for that organization Then preferred resources are ranked above non-preferred options with equivalent fit scores And preferred status is visible on suggestions and on the resource profile And users can override ranking for a single booking without changing the organization preference
Lead-Time-Sensitive Needs Flagged and Prioritized
Given an accommodation type has a configured lead time (e.g., ASL 7 days) When an event is created or updated Then cues whose lead-time window has started are flagged High Priority and added to the Captain’s action list And reminder tasks are scheduled at lead-time start and halfway to the event time And suggestions for lead-time-sensitive cues prioritize resources with earliest availability and highest reliability
Reliability Scoring and Post-Event Updates
Given a resource has prior bookings When a booking outcome is recorded (Show, Late, No-Show, Cancelled In Advance) Then the resource’s reliability score is recalculated using the last 12 months of outcomes with decay for older events And the reliability score is bounded 0–100 and updates within 5 seconds of outcome save And the next suggestions use the updated reliability score in ranking
Privacy & Consent Controls for Accommodations
"As a volunteer, I want my accommodation details handled discreetly and only shared with those who need to know so that I feel safe participating."
Description

Protect sensitive accommodation information with role-based visibility, explicit consent capture, and purpose-limited sharing. Allow volunteers to mark details as sensitive and control which roles (e.g., captains, coordinators) can view them. Log access events, support redacted views in checklists, and include data retention settings to minimize long-term exposure. Ensure data is encrypted in transit and at rest, and provide export-with-redaction for compliance requests. Reflect consent state in cue generation so only necessary details are shown to fulfill the cue.

Acceptance Criteria
Role-Based Visibility & Volunteer Controls for Sensitive Details
Given a volunteer adds or edits accommodation details in the profile or event signup When they toggle "Mark as sensitive" and select allowed viewer roles (e.g., Captain, Coordinator, Admin) Then the system requires at least one role selection (or prevents save) and stores sensitivity flag, selected roles, and timestamp And shows a Role Preview that displays Full vs Redacted views per role before saving And, after saving, users with allowed roles see the full value; all others see "[Redacted]" plus a reason ("not permitted") in roster, profile, and Captain Cues And any denied view attempt is blocked without leaking content length or hints and is logged as a denied access event
Explicit Consent Capture and Revocation Lifecycle
Given a user enters accommodation details for a volunteer When saving Then the UI requires explicit consent selection for "use to operate events" and shows purpose text; default is unselected And the system records consent state, timestamp, actor, channel, and consent text version And provides a self-service revoke link in volunteer portal and an admin revoke action When consent is revoked Then sensitive details become hidden from all non-admin roles, cues stop including those details, and affected captains/coordinators receive a notification to adjust operations And the audit log records revocation and all subsequent enforcement outcomes
Consent-Aware Cue Generation in Captain Cues
Given a Captain opens Captain Cues for an upcoming event with volunteers requiring accommodations When a cue can be fulfilled without exposing underlying sensitive detail Then render only the minimal actionable instruction (e.g., "Reserve front-row seat") and omit diagnosis/notes And if the Captain role is not in the allowed viewers for that detail or consent is missing Then the cue either (a) renders as a redacted placeholder with "needs coordinator review" or (b) is not generated, according to org policy And tapping One-Tap Done completes the checklist item without ever revealing redacted content And all cue decisions (shown/minimized/redacted/suppressed) are recorded with rationale for audit
Redaction Across Roster, Search, and Notifications
Given sensitive accommodation details exist with restricted roles When a disallowed role views rosters, searches by keywords, or receives notifications Then search results and notifications must not include the sensitive value or derived hints; only neutral labels (e.g., "Accommodation on file") are shown And roster columns display a redacted badge without field length or format leakage And deeplinks to sensitive content require authorization checks on open and do not include sensitive values in URLs
Data Retention Settings and Auto-Purge
Given an org admin sets accommodation data retention to N days and selects purge action (delete or pseudonymize) When a record reaches its retention date or consent is revoked Then the system executes the configured action within 24 hours, removes attachments, updates indexes/search, and refreshes related cues And produces a purge log with record IDs, action, actor (system), timestamp, and counts, available to admins And ensures purged content is excluded from exports and removed from online backups within 30 days
Compliance: Access Logging and Redacted Export
Given any view, edit, export, or denial involving sensitive accommodation data Then the system writes an immutable, signed audit event with actor ID, role, subject ID, field, action, result (allow/deny), purpose tag, client/app, IP, and timestamp And admins can filter audit logs by subject, actor, action, and date range and export results within 2 minutes for up to 50k events When a data subject access request is initiated Then the system generates within 2 minutes a downloadable package containing: redacted volunteer data (CSV/JSON), a human-readable PDF, and the last 12 months of relevant audit events And all downloads are protected by expiring links (24 hours), single-use tokens, and are logged
Encryption in Transit and At Rest
Given any transport of accommodation data between clients and servers Then TLS 1.2+ (preferring 1.3) with HSTS and secure ciphers is enforced; plaintext HTTP is redirected or blocked And all accommodation fields and attachments are encrypted at rest using AES-256 with cloud KMS-managed keys; keys are rotated at least every 90 days And mobile offline caches store sensitive data encrypted with OS keystore; app avoids writing sensitive values to logs or crash reports And backup snapshots are encrypted; restore tests verify encrypted backups are recoverable

Access Routes

Sends personalized arrival links with accessible entrances, elevator codes, quiet rooms, and restroom notes, matched to each volunteer’s badges and the venue template. Works offline with cached maps and SMS directions for spotty service. Reduces late check-ins and makes first contact feel welcoming and clear.

Requirements

Accessibility Badge Matching Engine
"As a volunteer with mobility needs, I want my arrival instructions to automatically surface step-free routes so that I can reach check-in without barriers."
Description

Builds personalized arrival instructions by matching each volunteer’s accessibility badges (e.g., mobility, sensory, language, neurodiversity) to the most suitable paths and amenities from the venue template. Applies precedence rules and fallbacks when data is incomplete, merges multiple badge needs into a single coherent route, and flags conflicts for organizer review. Integrates with volunteer profiles and event assignments, respects privacy/consent settings, and updates links dynamically if badges or venue details change.

Acceptance Criteria
Single Badge Route Generation (Mobility)
Given a volunteer with a Mobility badge is assigned to an event at a venue with at least one wheelchair-accessible entrance and elevator access When the engine generates arrival instructions Then the selected path includes only accessible segments (0 stairs) And the instructions include the accessible entrance name and any required elevator code if available And the ETA and distance reflect the accessible path And a shareable arrival link is created and stored on the volunteer’s event assignment and returns HTTP 200 on retrieval
Multi-Badge Merge into Coherent Route
Given a volunteer has Mobility, Sensory-Low-Noise, and Language: Spanish badges and the venue template includes multiple entrances, a quiet room, and multilingual notes When the engine generates arrival instructions Then a single route is produced that satisfies all applicable badges without contradictory steps And quiet room and accessible restroom notes are included once without duplication And instruction language is Spanish, with fallback to English only where Spanish copy is missing and those segments are explicitly marked "(EN)" And the final instruction set passes schema validation and contains no empty fields
Precedence Rules and Data Fallbacks
Given a volunteer’s badges imply conflicting preferences and the venue template is missing one or more optional fields (e.g., elevator code) When the engine applies the configured precedence rules Then higher-precedence needs are honored and the applied rule-set ID is recorded on the assignment And missing optional data triggers documented fallbacks with a visible "Fallback used" note in the instructions And a telemetry event "ac_route_fallback" is emitted with venueId, eventId, volunteerId, and missingField details
Conflict Detection and Organizer Flagging
Given a volunteer has a Mobility badge requiring elevator access and the venue template marks the elevator as Out of Service for the event timeframe When the engine generates arrival instructions Then the assignment is flagged "Route Conflict" and the route is not auto-sent And an organizer review task is created with severity High within 10 seconds And the arrival link (if accessed) displays "Route pending organizer review" And a notification is sent to the event owner via in-app and email channels
Privacy and Consent Enforcement
Given a volunteer has Mobility (internal-only) and Language: Spanish (shareable) badges per profile consent settings When the engine generates arrival instructions and link content Then routing decisions use both badges And public-facing instructions display in Spanish and omit mobility-specific labels, icons, or explicit disability terms And link payload includes only consented badge metadata; internal-only badges are not embedded and are referenced only by internal IDs server-side And access logs record viewer identity with PII minimization and no badge values beyond consented scope
Dynamic Updates on Badge or Venue Change
Given arrival instructions and a link have been generated for an upcoming shift When the volunteer updates badges or the organizer updates venue path/amenity details Then the route is regenerated within 60 seconds of save And the existing link redirects (HTTP 302) to the latest version And the assignment record increments routeVersion and updates updatedAt And the volunteer receives an updated SMS if a prior link was sent within the last 24 hours
Profile and Assignment Integration
Given a volunteer without any event assignment When the engine is invoked Then no arrival link is generated and a "No Assignment" reason is logged Given the same volunteer is later assigned to two overlapping shifts at the same venue When the engine runs Then two distinct links are generated, each bound to its assignmentId And in the mobile app, each link appears under the corresponding shift card only
Venue Accessibility Template Builder
"As an organizer, I want to save a reusable venue accessibility template so that volunteers always receive accurate arrival guidance."
Description

Provides an organizer-facing editor to capture and maintain reusable accessibility data per venue, including entrances with attributes (step-free, ramp, width), elevator locations and codes with time windows, quiet rooms, restroom types and proximity, parking/drop-off points, indoor waypoints, and floor plans. Supports cloning and versioning, validation for required fields, preview by persona (e.g., wheelchair user, low-vision), and bulk import from spreadsheets. Templates tie to events and power the content of personalized links.

Acceptance Criteria
Create and Validate New Venue Template
Given I am an organizer with edit permissions When I create a venue template and provide: name; at least one entrance (attributes: step-free boolean, ramp boolean, door width >= 32 inches); at least one elevator (location, code of 4–8 digits, time window HH:MM–HH:MM); at least one quiet room (capacity >= 1); at least one restroom (type in [gendered, gender-neutral, accessible], proximity in meters); at least one parking/drop-off point; at least one indoor waypoint; and a floor plan file (PDF/JPG/PNG <= 10 MB) And I click Save Then the system validates required fields and data types and displays inline errors for any missing/invalid values without saving And when all validations pass the template is saved with status Draft, a unique template ID, created_at, updated_at, and version 1.0.0
Versioning and Immutable History
Given a published venue template at version 1.0.0 with at least one event assigned When I edit the template and choose Publish new version Then a new version 1.1.0 is created and previous versions remain read-only and immutable And an audit log records editor, timestamp, and field-level diff And events already bound to 1.0.0 remain on 1.0.0 until explicitly re-bound And Compare view shows differences between 1.0.0 and 1.1.0 And Revert creates a new version that restores the selected prior content
Clone Template for a New Venue
Given a source venue template with entrances, elevators, quiet rooms, restrooms, parking, waypoints, and floor plans When I select Clone Then a new template is created with a new ID, name prefixed "Copy of", status Draft, and version 1.0.0 And all sections and attachments are duplicated to the new template And no events are associated with the cloned template And the cloned template must pass validation before it can be published
Persona-Based Preview (Wheelchair, Low-Vision)
Given a venue template with multiple entrances, elevator details with time windows, and restroom metadata When I open Preview and select persona "Wheelchair user" Then the preview shows only step-free entrances and routes, includes elevator codes valid for the event time window, flags any door width < 32 inches, and orders routes by shortest step-free path When I switch persona to "Low-vision" Then the preview renders high-contrast text with minimum 18pt font, includes alt text for floor plans, avoids color-only distinctions, and provides SMS-friendly step directions capped at 160 characters each
Bulk Import from Spreadsheet with Error Reporting
Given I have a CSV/XLSX file and mapping for columns to template fields When I upload and start an import of up to 5,000 rows Then each row is validated independently; valid rows are imported; invalid rows are skipped And a downloadable error report lists row number, field, and message for each failure And imported templates are created as Draft with unique IDs and version 1.0.0 And duplicates are prevented using external_id + venue name matching across files and runs And the import completes within 2 minutes for 5,000 rows at p95
Bind Template Version to Event and Generate Access Routes Payloads
Given an event and a published venue template version V When I assign version V to the event and publish Access Routes Then personalized arrival link payloads are generated from version V and filtered by volunteer accessibility badges (e.g., wheelchair) And elevator codes are included only when within valid time windows And updating the template to V+1 does not change existing published links until I explicitly republish And the payload API responds within 300 ms at p95 and includes Cache-Control headers
Personalized Arrival Link Generation & Delivery
"As a volunteer, I want a single link with everything I need to arrive so that I don't have to search through multiple messages."
Description

Creates unique, signed, expiring URLs per volunteer and event that render tailored arrival instructions on mobile web or deep-link into the app when installed. Generates short links and QR codes, injects them into confirmation and reminder messages (SMS, email, push), and tracks opens and last-viewed timestamps. Content updates propagate without changing the link. Links respect permission scopes, hide sensitive codes until arrival window, and support localization and screen-reader accessibility.

Acceptance Criteria
Unique Signed Expiring URL per Volunteer-Event
Given a volunteer V and event E, when generating an arrival link, then a unique URL is produced that is scoped to V and E And the URL contains a signed token that becomes invalid if any part of the URL is altered And the token expires at the configured time; after expiry, requests return an expired response and do not expose arrival content And the link for V cannot access content for any other volunteer or event
Deep Link to App with Web Fallback
Given the GiveCrew app is installed and can handle deep links, when the arrival link is opened, then the app opens directly to the event’s arrival screen Given the app is not installed or deep link handling fails, when the arrival link is opened, then a mobile web arrival page is rendered with equivalent content And the user can choose to continue on the web if the OS or browser blocks deep linking
Short Link and QR Generation and Message Injection
Given a signed arrival URL exists, when generating communications, then a short link is created that redirects to the signed URL And a QR code image is generated that encodes the signed URL And the short link and QR are injected into SMS, email, and push templates in the designated placeholders And the email includes alt text for the QR image and the SMS body length remains within the configured character limit
Open Tracking and Last-Viewed Timestamps
Given a personalized arrival link, when it is accessed, then an open event is recorded with timestamp and channel (SMS, email, push, other) if available And subsequent accesses increment open_count and update last_viewed_at to the most recent timestamp And tracking occurs when the app handles the deep link without exposing PII in URLs And open events are queryable per volunteer and event for reporting
Content Updates Propagate Without Link Change
Given arrival instructions are updated after links were sent, when the volunteer opens or refreshes the link, then the latest content is displayed without issuing a new URL And short links and QR codes previously sent continue to resolve to the updated content And caches invalidate so updates are visible to end users within 60 seconds of publish
Permission Scopes and Time-Gated Sensitive Codes
Given a volunteer lacks permission for specific fields, when opening the link, then restricted fields are omitted or masked Given the arrival window has not started, when opening the link, then sensitive codes (elevator, door) are hidden Given the arrival window is active and the volunteer has permission, when opening the link, then sensitive codes are revealed And API requests using the link token outside the allowed window or scope return a 403-equivalent response and do not leak values
Localization and Screen-Reader Accessibility
Given the user’s locale from device or profile, when opening the link, then all text, dates, times, numbers, and directionality render in that locale And a language switcher is available when multiple locales are configured And the page is navigable via keyboard, has proper landmarks, labels, and alt text, and passes WCAG 2.1 AA checks for screen readers And the QR code has descriptive alt text and the link has an accessible name that indicates its purpose
Offline Maps and Directions Caching
"As a field volunteer with spotty service, I want the directions to work offline so that I can still find the accessible entrance."
Description

Enables client-side caching of map tiles, floor plans, and text step-by-step directions for the last-mile approach and indoor navigation. Pre-downloads assets when the link is opened or via scheduled background sync, with storage limits, eviction policies, and integrity checks. Provides a text-only fallback when rich assets are unavailable, displays an offline indicator with last-updated time, and is optimized for low-end Android devices and spotty connectivity conditions.

Acceptance Criteria
Pre-download on Link Open and Scheduled Sync
Given a volunteer opens an Access Routes link while online When the page loads Then background caching of last‑mile map tiles (1 km radius), venue floor plans, and text step‑by‑step directions starts within 3 seconds And assets are stored per event and user for offline use And available storage is checked; if insufficient, the app prioritizes text directions and a reduced 300 m map radius And Given background sync is enabled and the device is on Wi‑Fi or charging When it is within 15 minutes of an upcoming shift Then a scheduled sync refreshes or pre‑downloads required assets without duplicating up‑to‑date files (validated via ETag/hash)
Offline Access in Airplane Mode (Last‑Mile + Indoor)
Given cached assets exist for Event X And the device is in airplane mode When the volunteer opens the Event X arrival link Then the page renders without network calls And the last‑mile map loads from cache within 2 seconds And the indoor floor plan is viewable with pan and zoom And text step‑by‑step directions are displayed And an Offline notice is visible
Cache Size Limit and Smart Eviction Policy
Given a cache size limit of 150 MB and assets for multiple events (some past, some within 72 hours, some beyond) When adding new assets would exceed 150 MB Then the system evicts least‑recently‑used assets excluding events occurring within the next 72 hours And if still over limit, reduces map tile radius in 100 m steps down to a 300 m minimum before evicting text directions And the resulting cache size is less than or equal to 150 MB
Asset Integrity Verification and Auto‑Recovery
Given each asset bundle is accompanied by a version and SHA‑256 checksum When a downloaded asset fails integrity verification Then the asset is discarded and re‑downloaded up to 2 retries with exponential backoff And if recovery fails, the UI switches to text‑only directions with an inline message that rich assets are unavailable And a telemetry event is logged with asset id, failure reason, and timestamp
Text‑Only Fallback with SMS Directions
Given rich assets are unavailable due to corruption or storage constraints When the arrival link is opened Then only text step‑by‑step directions are shown and cover the full route from last known cross street to the venue entrance And a Get directions via SMS action opens the SMS composer with a prefilled message containing the event code and venue keyword And the action works without data connectivity And no broken images or tile placeholders are rendered
Offline Indicator with Last‑Updated Timestamp
Given cached assets exist with a last successful sync time T When the page is offline Then an Offline badge is displayed with local timestamp in the format "Updated {MMM DD, HH:mm}" And the timestamp equals T ± 1 minute And tapping the badge shows a tooltip that content may be outdated and to reconnect to refresh And when connectivity resumes, the badge changes to Online within 5 seconds and last‑updated only changes after a successful refresh
Low‑End Android Performance and Resilience
Given a baseline Android device (Android 8+, 2 GB RAM) and cold start When opening the arrival link offline Then time‑to‑first‑interactive is less than or equal to 2.0 seconds And panning/zooming the cached map maintains at least 45 FPS for 90% of interactions And median app memory usage during the session remains less than or equal to 120 MB And background sync battery impact is less than or equal to 1% over 24 hours with one refresh And failed network requests are not surfaced to the user and are retried with exponential backoff when online
SMS Directions Fallback with Interactive Prompts
"As a volunteer without a smartphone data plan, I want directions via text so that I can still arrive on time."
Description

Delivers condensed, step-by-step arrival directions via SMS when data is poor or the user opts into text-only mode, including elevator/door codes and accessibility notes. Supports interactive keywords (MORE, REPEAT, AGENT), localized content, call-to-contact for shift leads, and quiet-hour rules. Implements A2P compliance with opt-in/out flows (START/STOP) and rate limits to prevent spam, with delivery metrics recorded for analytics.

Acceptance Criteria
SMS Fallback Trigger and Delivery Timing
Given a volunteer has a scheduled shift and a verified mobile number When data connectivity is unavailable or latency exceeds 2 seconds for 3 consecutive checks, or the volunteer has enabled text-only mode Then the system sends the first directions SMS within 5 seconds of detection Given fallback SMS is being sent When the message exceeds a single segment Then the content is split into at most 3 ordered segments labeled “1/3”, “2/3”, etc. Given the standard arrival link has been delivered in the last 2 minutes When connectivity stabilizes before sending SMS Then suppress the fallback SMS to avoid duplication and log the suppression event
Condensed Directions with Accessibility and Codes
Given the volunteer’s accessibility badges and the venue template exist When generating the directions SMS Then include venue name, shift start time, meet location, step-by-step entry instructions, elevator/door codes, and accessibility notes relevant to the volunteer’s badges, and exclude non-relevant notes Given directions are condensed for SMS When composing the message Then prioritize steps in the order: entrance → elevator/ramp → floor/room → check-in desk → quiet room → accessible restroom, and include a short call-to-action: “Reply MORE/REPEAT/AGENT” Given a code is included When the shift end time has passed Then no further messages containing that code are sent
Interactive Keywords: MORE, REPEAT, AGENT
Given a directions SMS was sent in the last 2 hours When the volunteer replies MORE (case-insensitive) Then send additional details (e.g., parking, alternate entrance, landmarks) in at most 2 segments within 5 seconds and record the interaction Given a directions SMS was sent in the last 2 hours When the volunteer replies REPEAT (case-insensitive) Then resend the most recent directions segment(s) within 5 seconds and record the interaction Given an assigned shift lead has a reachable phone number When the volunteer replies AGENT (case-insensitive) Then respond with the lead’s tap-to-call number and attempt an immediate connect; if the lead is unavailable, reply with fallback contact and queue the request for follow-up
A2P Opt-In/Out and HELP Compliance
Given the recipient has not opted into SMS for GiveCrew Access Routes When attempting to send directions Then send a single opt-in request stating program name, purpose, frequency, STOP/HELP/START keywords, and do not send directions until consent is recorded Given the recipient sends STOP When processing inbound SMS Then immediately confirm opt-out, cease all further messages, and persist the opt-out status Given the recipient sends START When processing inbound SMS Then confirm re-subscription and resume eligibility for messaging Given the recipient sends HELP When processing inbound SMS Then reply with program name, support contact, hours, “Msg & data rates may apply,” and STOP to opt out
Quiet Hours Enforcement with Event-Start Override
Given the recipient’s local quiet hours window is configured When a non-urgent outbound directions message would fall inside quiet hours Then defer sending until quiet hours end and log the deferral with reason “quiet-hours” Given the event start time is within 60 minutes and the recipient has an active shift When directions need to be sent during quiet hours Then send the message immediately with a “Time-sensitive” prefix and log reason “start-soon-override” Given user-specific preferences exist When quiet-hours enforcement runs Then honor the user’s explicit overrides for always-allow or always-defer
Rate Limiting and De-duplication
Given outbound SMS traffic per recipient is tracked When sending proactive (non-reply) messages Then enforce a per-recipient limit of at most 3 messages per 15 minutes and at most 10 messages per 24 hours Given the system is about to send content identical to a message sent in the last 10 minutes When evaluating the send Then suppress the duplicate and log a deduplication event Given the carrier signals 429/rate-limit or temporary failures When retrying delivery Then apply exponential backoff with jitter up to 3 retries and surface the event to monitoring Given campaign-level throughput constraints are configured per A2P registration When queueing bulk directions messages Then do not exceed the configured TPS; queue and drain within SLA
Localization of SMS Content
Given a recipient locale is set or inferred When generating an SMS Then render all content in the recipient’s language and formats (date/time, numerals), and fall back to en-US if the locale is unsupported Given venue template translations exist for multiple languages When composing accessibility notes and labels Then select the translation matching the recipient’s locale; if a label has no translation, use the default and mark a missing-translation metric Given a right-to-left language is selected When sending the SMS Then ensure text direction and punctuation are correct for RTL presentation
Arrival Verification, Reminders, and Analytics
"As an organizer, I want to know who is en route or delayed so that I can reassign roles and reduce late check-ins."
Description

Integrates arrival flow with Check-In by capturing geofence or link-tap "I'm here" signals and guiding volunteers to the desk or staging point. Schedules reminders based on event start and estimated travel time, escalating if links aren’t opened by a defined threshold. Surfaces real-time en-route/arrived status to organizers, records late-arrival reasons, and reports open rates and on-time arrival metrics to the Impact Board, with privacy-conscious location handling and user consent.

Acceptance Criteria
Arrival verification via geofence or link-tap
Given a scheduled volunteer with location consent enabled and an active event geofence of 100m radius When the device remains inside the geofence for ≥60 seconds or the volunteer taps a unique "I'm here" link Then the system marks the volunteer as Arrived with a timestamp and displays the assigned desk/staging point And deduplicates multiple signals within 10 minutes to a single arrival event And if both geofence and link-tap occur, the earliest valid signal sets the arrival time And the status is synced to the organizer view within 10 seconds
ETA-based reminder scheduling with escalation
Given event start time T and a volunteer with a calculable ETA from last consented location or a fallback ETA of 45 minutes When scheduling reminders for the shift Then send Reminder 1 at R1 = max(T − ETA − 20 minutes, now + 5 minutes), constrained between (T − 2 hours) and (T − 30 minutes) And if the arrival link is not opened within 15 minutes of R1, send Reminder 2 via SMS And if the link remains unopened by T − 15 minutes, flag the volunteer as Unreached and notify organizers And opening the arrival link at any time cancels pending reminders for that shift
Organizer dashboard shows real-time en-route/arrived status
Given organizers viewing the event dashboard When a volunteer opens the arrival link or enters the geofence Then the dashboard shows status En route with last signal time and ETA, updating at least every 60 seconds while active And upon arrival confirmation, status changes to Arrived and the volunteer appears in the Arrived list And organizers can filter by event, shift, and role and see counts of En route, Arrived, and Unreached And no precise coordinates are displayed to organizers
Guided directions to check-in desk/staging with accessibility
Given a volunteer marked Arrived and a venue template with desk/staging points and accessibility data When the arrival screen is presented Then show step-by-step directions to the assigned desk/staging point with accessible routes matched to the volunteer’s badges And include elevator codes, accessible entrances, quiet room, and restroom notes when applicable And if network quality is poor, automatically fall back to cached maps and send SMS directions within 10 seconds
Late-arrival reason capture at check-in
Given an arrival timestamp later than scheduled start time + 5 minutes When the volunteer proceeds to complete check-in Then prompt the volunteer to select a late-arrival reason from a predefined list with optional free-text note And require a selection to finalize check-in And store the selected reason and minutes late for reporting and export
Impact Board reporting for opens and on-time arrivals
Given completed events with reminder and arrival data When generating Impact Board metrics Then display per-event and rolling 7-day reminder open rate by channel, arrival link click-through rate, and on-time arrival percentage (arrived between T − 5 minutes and T + 5 minutes) And update metrics within 5 minutes of event start and finalize within 15 minutes after event end And exclude volunteers who declined reminders or canceled shifts from denominators And reconcile displayed metrics with raw logs within 1% variance
Privacy-conscious consent and data retention
Given a volunteer without prior location consent When requesting permission for arrival verification Then present a clear purpose statement, data retention policy (delete raw location pings within 24 hours), and opt-out controls, requiring explicit consent to enable geofence tracking And if consent is denied or later revoked, only link-tap arrival is available and geofence tracking remains disabled And organizer views and the Impact Board expose only statuses and aggregated metrics, never individual coordinates And volunteers can request deletion of location-derived records, which is completed within 72 hours

Need-to-Know Badges

Protects privacy by showing accommodation badges only to roles that need them (e.g., Captains see details; peers see minimal indicators). Volunteers can set visibility comfort levels, and Consent Ledger records scope and changes. Maintains dignity and trust while ensuring the right support gets delivered.

Requirements

Role-Based Badge Visibility Engine
"As a shift captain, I want to see only the accommodation details necessary for my role so that I can prepare support without exposing volunteers’ private information to others."
Description

Implements a policy-driven rules engine that determines which badge fields are visible to each role (e.g., Captains, Peers, Schedulers) across mobile and web surfaces. Enforces data minimization by filtering sensitive attributes and exposing only the minimum necessary details to fulfill the role’s operational duties. Supports context-aware scopes (event, shift, team), default privacy-first settings, and real-time evaluation on roster, chat, and assignment views. Integrates with existing auth/roles, logs all access decisions, and gracefully degrades offline with cached policies. Outcome: Captains see actionable accommodation details; peers see only minimal indicators, preserving dignity while enabling support.

Acceptance Criteria
Captain Roster View Shows Actionable Badge Details
Given a logged-in user with role Captain and an event roster containing volunteers with accommodation badges and the volunteer's comfort level permits Captain access to details When the Captain opens the roster view for that event Then only the badge fields explicitly allowed for Captain on the roster surface are returned by the API and rendered, and disallowed fields (e.g., notes, attachments, raw medical terms) are absent from both payload and DOM And the number of badge fields displayed equals the allowlist count for the Captain/roster policy And action controls associated with those badges (e.g., "Mark support provided") are enabled if and only if allowed by policy
Peer Views Show Indicator-Only
Given a logged-in user with role Peer and a volunteer with one or more accommodation badges When the Peer views the volunteer on roster or opens a 1:1 or group chat containing that volunteer Then only a non-descriptive badge indicator (icon and/or count) is visible, and no badge titles, summaries, or notes are present in the API response or DOM And tapping the indicator reveals only the guidance "Contact a Captain for details" and does not expose any badge metadata or IDs And any attempt to fetch badge details returns HTTP 403 with reason "INSUFFICIENT_SCOPE"
Scheduler Assignment Screen Honors Event and Shift Scope
Given a logged-in Scheduler assigned to Team T for Event E1 and not assigned to Event E2, and a volunteer with badges assigned to shifts S1 (E1) and S2 (E2) When the Scheduler opens the assignment screen for S1 Then only the badge fields allowed for Scheduler on the assignment surface within Team T scope are returned and rendered And when the Scheduler attempts to open S2 (E2), the engine renders indicator-only and any badge detail API call responds 403 And within E1, volunteers outside Team T show indicator-only
Consent Comfort Levels Override Role Defaults
Given a volunteer sets comfort levels: Captains=Details, Peers=Indicator-Only, Schedulers=Summary When a Captain, a Peer, and a Scheduler view that volunteer on roster, chat, and assignment respectively Then each sees exactly the level specified by the volunteer's comfort level, even if the global role policy would otherwise permit more And when the volunteer changes the comfort level to Captains=Summary in the app Then all open views re-evaluate within 60 seconds and reflect the new level without requiring app restart And the Consent Ledger records an entry with timestamp, actor, prior value, new value, and scope (event/shift/team)
Real-Time Re-evaluation on Context Change Across Surfaces
Given a user with role Captain has the roster for Event E1 Team T open with volunteers visible When the user navigates to chat with a listed volunteer, then to the assignment surface for Shift S1, and then filters to Team U Then the visibility engine re-evaluates on each transition and updates the badge presentation within 200 ms of the surface/scope change And any badge details visible in the prior context that are not permitted in the new context are immediately removed from the UI and not retained in client cache And a fresh API request is issued for each new context; responses from prior contexts containing disallowed fields are not reused
Offline Operation With Cached Policies and Deny-by-Default
Given the device is offline and a signed policy cache exists dated less than 24 hours old When a user views roster or assignment surfaces Then visibility is evaluated using the cached policies and any field not explicitly allowed is denied, showing indicator-only fallback And access decision logs are queued locally for upload on reconnect, and no badge details are written to persistent storage And if the cache is older than 24 hours, all roles receive indicator-only and detail requests are blocked client-side
Access Decision Logging and Auditability
Given any visibility decision (ALLOW, REDACT, DENY) occurs on any surface When the decision is evaluated Then a log entry is created with actor ID, role, surface, scope (event/shift/team), policy version, anonymized badge reference, decision, and reason And 100% of decisions while online are transmitted to the audit service with at-least-once delivery; duplicates carry an idempotency key And logs are queryable by volunteer, role, surface, and time range via existing reporting endpoints
Volunteer Visibility Controls UI
"As a volunteer, I want to control who can see my accommodation information so that I feel safe participating and can share only what’s needed."
Description

Provides volunteers with an intuitive interface to set per-badge visibility preferences (e.g., Captains-only, Team leads, Minimal indicator for peers), with clear microcopy and examples. Offers per-event overrides, quick privacy presets, and a preview mode showing how others will see their badges. Defaults to most private reasonable settings, with just-in-time education and consent prompts. Syncs across devices, supports localization and accessibility, and prevents collection of unnecessary medical details by guiding users toward needs-based statements (e.g., "needs seated station" vs. diagnosis). Outcome: Increased trust and adoption through transparent, self-directed privacy.

Acceptance Criteria
Per-badge visibility selection and consent logging
Given I am a logged-in volunteer with at least one accommodation badge When I set a badge to Captains-only, Team Leads, or Peers Minimal in Visibility Controls Then the selection is saved within 1 second and reflected in the badge detail UI And the setting persists across logout/login and device change for the same account And an entry is written to the Consent Ledger with userId, badgeId, previousScope, newScope, timestamp, and eventContext (if applicable)
Per-event visibility override with automatic reversion
Given I have global visibility defaults configured When I create an event-specific override for a badge Then the override applies only to that event and supersedes the global setting And the UI surfaces an override chip showing the event name and scope And I can clear the override in one tap/click and see a confirmation And at event end time the system reverts to the global setting and records the reversion in the Consent Ledger
Privacy presets with explicit mappings
Given presets Private, Balanced, and Open are available When I preview a preset Then I see a diff of changes by role before confirming When I apply Private Then all badges map to Captains-only and peers see no indicator When I apply Balanced Then Captains and Team Leads see details and peers see a minimal indicator only When I apply Open Then Captains and Team Leads see details and peers see a minimal indicator; no diagnosis details are shown to peers And I can undo the last preset within 10 seconds
Role-based preview mode
Given I open Preview mode When I switch between Captain, Team Lead, and Peer Then I see exactly what that role would see for each badge (including minimal indicators for peers) And no edit controls are visible in Preview And exiting Preview returns me to the same tab and scroll position
Default privacy and consent education
Given I am first-time configuring visibility or adding a new badge Then default visibility is Captains-only; peers see no indicator until explicitly enabled And an inline education tip explains impacts of expanding visibility with at least two localized examples When I expand visibility beyond default Then a consent prompt summarizes who will see what and requires explicit confirmation And the consent decision plus summary is recorded to the Consent Ledger
Needs-based guidance and PHI minimization
Given I type free text about an accommodation When my text includes diagnosis or PHI keywords from the configured blocklist Then the UI prompts me to rephrase as needs-based (e.g., "needs seated station", "requires quiet space") with suggestion chips And saving is blocked if prohibited categories are enabled; otherwise I can proceed after acknowledging And saved text is scanned to confirm no diagnosis keywords remain And the system stores only needs-based text and visibility scope; no diagnosis fields are stored
Accessibility, localization, and cross-device sync
Given a keyboard-only or screen reader user When navigating the Visibility Controls UI Then all controls are reachable in order, have names/roles/states, and meet WCAG 2.2 AA; color contrast >= 4.5:1; focus is visible And the UI is fully localized in at least English and Spanish, with RTL support and no truncation on common mobile viewports And role labels, preset names, and example microcopy are localized Given I update a setting on Device A When I open the same account on Device B with connectivity Then the change appears within 10 seconds or after manual refresh, whichever is sooner
Consent Ledger with Versioning
"As an organization admin, I want an auditable history of volunteers’ consent settings so that we can demonstrate compliance and honor changes promptly."
Description

Creates an immutable, append-only ledger recording consent scope, purpose, timestamps, actor, channel (app/web/import), and contextual linkage to events and badges. Supports revocation, retroactive scope tightening, and evidence export (PDF/CSV) with cryptographic integrity checks. Surfaces change history in admin tools, triggers policy re-evaluation on updates, and notifies affected users when scope changes impact visibility. Data stored with encryption at rest and strict retention controls, aligning with organizational compliance needs. Outcome: Verifiable, auditable consent trail that operationalizes trust and compliance.

Acceptance Criteria
Capture New Consent Across Channels
- Given a volunteer submits consent via mobile app, web form, or bulk import, when the consent is saved, then a new append-only ledger entry is created with: consent_id, version=1, scope, purpose, server_timestamp (UTC), actor_id, actor_role, channel in {app, web, import}, contextual_links (event_id and/or badge_id if applicable), and entry_hash. - The server assigns the timestamp; client-supplied timestamps are ignored. - No API or UI can modify or delete the entry; any attempted update returns 405 Method Not Allowed and is recorded in a security audit log.
Record Revocation or Scope Tightening as New Version
- Given an existing consent record, when a user revokes consent or tightens visibility scope, then the system appends a new ledger entry with version incremented by 1, change_type in {revocation, scope_tighten}, previous_version reference, effective_at timestamp, and updated scope. - The prior versions remain immutable and readable, but the current effective scope used by policy engines reflects the latest version immediately after effective_at. - Retroactive scope tightening is enforced: all policy evaluations use the latest scope for both future and past contextual links within 60 seconds of change.
Tamper-Evident Hash Chain and Verification API
- Each ledger entry stores prev_hash (hash of prior version’s canonicalized content) and entry_hash (hash of current canonicalized content) using SHA-256. - The verify endpoint for a consent_id returns {status: "valid"} when all hashes and version sequence are intact, and {status: "invalid", error_code} when any link is broken or missing. - Altering any stored field of an entry without appending a new version causes the verify endpoint to return "invalid".
Evidence Export to PDF/CSV with Integrity Manifest
- Given an admin selects a person or date range, when export is requested, then the system generates CSV and PDF files containing all ledger entries and a separate manifest.json including chain_root_hash, export_timestamp, and a digital signature verifiable with the organization’s public key. - The export includes a human-readable change timeline and machine-readable rows with all required fields (consent_id, version, scope, purpose, actor_id, actor_role, channel, timestamps, contextual_links, change_type, prev_hash, entry_hash). - For exports up to 10,000 entries, files are available for download within 60 seconds, or the system surfaces a progress job with completion ETA.
Admin UI: Change History, Filters, and Diffs
- The admin history view lists all versions for a consent_id in reverse chronological order with key fields and badges/events links. - Admins can filter by actor, channel, change_type, date range, and person; results are consistent with exports. - Selecting two versions shows a field-level diff highlighting changes to scope, purpose, and contextual links.
Policy Re-evaluation and Impact Notifications
- When a consent version is added that changes visibility scope or revokes consent, the policy engine re-evaluates affected Need-to-Know badge visibility and access control within 60 seconds. - The volunteer and all affected Captains/admins receive notifications per their preferences (in-app and email) summarizing the change, effective_at, and impacted badges; delivery outcomes are recorded (sent, delivered, failed) with timestamps. - Users who no longer have access after re-evaluation immediately lose access on next authorization check and active sessions reflect changes within 5 minutes.
Encryption at Rest and Retention Controls
- All ledger data (including manifests and exports stored at rest) is encrypted using AES-256 with keys managed by KMS; key rotation occurs at least every 90 days; access to keys is restricted by role-based policies. - Retention policies are configurable per organization (e.g., 3 years); a nightly purge job deletes entries past retention unless on legal hold; each purge writes a new purge_log entry with counts and ranges deleted. - Applying or removing a legal hold on a person or consent_id prevents or permits purges accordingly and is itself recorded as a ledger entry with change_type=legal_hold_update.
Minimal Indicator Badges
"As a peer volunteer, I want subtle indicators that someone may need support so that I can be considerate without learning private details."
Description

Defines and renders non-disclosing badge indicators for non-privileged viewers, using neutral icons, color-safe palettes, and inclusive language. Tooltips and screen reader labels communicate action-oriented cues (e.g., "May need quiet space") without revealing sensitive personal details. Works consistently across roster tiles, check-in screens, chats, and printouts (Impact Board/roster sheets). Respects offline modes and printing constraints while meeting WCAG 2.1 AA. Outcome: Peers receive gentle, dignity-preserving cues to be supportive without accessing private information.

Acceptance Criteria
Roster Tiles Minimal Badge Display
Given I am a non-privileged viewer on the roster screen When a volunteer has one or more accommodation flags Then I see only neutral minimal indicator icons from the approved Minimal Indicators set And no text reveals specific conditions, diagnoses, or PII And each indicator exposes a tooltip/aria-label with an action-oriented cue (e.g., "May need quiet space") And the indicators’ order is consistent with other contexts for the same volunteer
Check-In Screen Non-Disclosure
Given I am on the event check-in screen as a non-privileged user When I open a volunteer’s check-in card Then the same minimal indicators appear as on the roster tile with identical order and icons And no link, button, or gesture reveals detailed accommodation data And long-press/copy/inspect actions do not expose hidden sensitive text beyond generic labels
Chat Header Minimal Indicator Rendering
Given I am viewing a chat thread that includes volunteers as a non-privileged user When participant chips/avatars/names are rendered Then minimal indicators display adjacent to the participant with the same icons, order, and labels as on roster tiles And indicators do not inject sensitive text into the message body or notifications And tooltips/labels are available on focus/long-press without revealing private details
Accessibility and Inclusive Language (WCAG 2.1 AA)
Rule: All minimal indicators are perceivable and operable via keyboard and assistive technologies (focusable, readable, dismissible). Rule: Non-text contrast for indicator icons and focus states is ≥ 3:1; any accompanying text/labels meet ≥ 4.5:1. Rule: Color is not the sole means of conveying meaning; each indicator is also differentiated by shape/outline/pattern. Rule: Tooltip/aria-label content uses an approved, action-oriented, inclusive phrase list and contains no diagnoses, conditions, or PII. Rule: Indicators are announced with concise labels (≤ 80 characters) and do not trap focus.
Printouts and Impact Board Grayscale Rendering
Given I generate an Impact Board or roster sheet for print/PDF When minimal indicators are included in the output Then indicators render in grayscale with shape/outline differentiation so meaning does not rely on color And no sensitive tooltip text is printed; an optional legend uses generic, action-oriented phrases only And indicators align consistently within the layout (no overlap/clipping) and match the set/order shown digitally for the same volunteer
Offline Rendering and Sync Integrity
Given the device is offline When I view roster tiles, check-in cards, or chat headers Then minimal indicators render from local, non-sensitive assets without needing network calls And changes to consent/visibility do not increase indicator disclosure until a successful sync confirms permissions And upon reconnection, indicators update to the correct minimal set within one sync cycle without flashing detailed data
Break-Glass Emergency Access
"As an on-site safety lead, I want a temporary emergency access option so that I can respond appropriately during incidents while maintaining accountability."
Description

Introduces a time-bound, purpose-limited override to reveal essential accommodation details during incidents. Requires justification entry, optional second approver for high-sensitivity badges, and auto-expiry with comprehensive audit logging. Notifies the volunteer post-incident (where appropriate), and updates the Consent Ledger with the event. Policies configurable by org (who can break glass, cooldowns, and escalation). Outcome: Safe, accountable access in emergencies without normalizing unnecessary exposure.

Acceptance Criteria
Authorized Initiation and Justification
Given the user has a role permitted by org policy to use Break-Glass When the user attempts to access a volunteer’s accommodation details via Break-Glass Then the system requires a justification text meeting the configured minimum length And displays the active policy (scope, duration, cooldown, and approver rules) And prevents proceeding until the user affirmatively confirms the warning And records the initiation attempt with timestamp and user identity in the audit log
High-Sensitivity Second Approver Flow
Given the targeted badge(s) are marked high-sensitivity and org policy requires a second approver When the initiator submits a Break-Glass request with justification Then the system routes the request only to eligible approvers defined by policy And blocks access until approval is granted or the request expires per SLA And records the approver’s decision, identity, and timestamps in the audit log And denies access if approver declines or SLA expires, updating the request status accordingly
Time-Bound Access and Auto-Expiry
Given a Break-Glass request has been approved (or does not require approval per policy) When access is granted Then only the scoped accommodation fields are revealed for the configured duration And a visible countdown timer shows remaining access time And upon expiry, further access attempts are denied without re-initiating Break-Glass And any issued access tokens are revoked and the expiry event is logged
Scope Minimization and Non-Normalization
Given an incident type and user role context are provided When Break-Glass access is granted Then only the minimum necessary fields mapped by policy to that incident type are displayed And export, bulk actions, API fetches for additional fields, and offline caching are disabled for the session And search results outside the scoped volunteer/fields are not expanded by Break-Glass And UI clearly indicates temporary access to discourage normalization of use
Cooldown and Re-Access Controls
Given a Break-Glass event for the same volunteer was completed recently When the same user or team attempts Break-Glass again within the configured cooldown Then the system blocks the attempt and displays the remaining cooldown time And provides an escalation path only if defined by policy (e.g., higher approver class) And logs the blocked attempt with reason and actor in the audit log
Comprehensive Audit Log and Consent Ledger Update
Given any Break-Glass lifecycle event (initiation, approval decision, grant, field views, expiry, notification) When the event occurs Then the system writes an immutable audit record including actor, volunteer, purpose/justification, policy version, approver (if any), fields accessed, timestamps, device/IP, and outcome And appends an entry to the Consent Ledger capturing scope, lawful basis, and event linkage And makes these records queryable to authorized org admins for compliance reporting
Post-Incident Volunteer Notification Rules
Given org policy requires notifying the volunteer and no suppression flags (e.g., harm risk, legal hold) are active When Break-Glass access expires Then the system sends a notification within the configured timeframe to the volunteer’s preferred channel And the message includes who accessed, when, purpose, and what categories of data were viewed, plus a dispute/feedback link And failed deliveries are retried per policy and ultimately logged as failed with reason And if suppression is active, no message is sent and the suppression reason is recorded in the audit and Consent Ledger
Badge Schema and Data Governance
"As a product admin, I want a governed badge schema with clear sensitivity rules so that data stays consistent, minimal, and secure across workflows."
Description

Establishes a standardized badge taxonomy (e.g., mobility, sensory, communication, medical alerts, language, pronouns) with field-level sensitivity classification, data minimization rules, and retention policies. Provides validation to capture accommodations as needs/actions rather than diagnoses, localization of labels, and cross-platform consistency. Exposes secure CRUD APIs and import/export pathways, with key management for encrypted fields. Integrates with shift assignment logic and reporting while preventing leakage into non-privileged views. Outcome: Consistent, interoperable badge data that is secure, minimal, and useful operationally.

Acceptance Criteria
Standardized Badge Taxonomy and Sensitivity Classes
Given I am a Schema Admin, When I create or edit a badge definition, Then I must provide machine_key (snake_case, unique), category in {mobility,sensory,communication,medical_alerts,language,pronouns,other}, field_type in {boolean,enum,text,date}, sensitivity in {Public,Restricted,Confidential}, and allowed_values for enum; otherwise the request is rejected with 422. Given an existing taxonomy, When I attempt to save a duplicate machine_key, Then the save fails with 409 and no changes are persisted. Given a schema change, When I publish, Then a new schema_version is created with timestamp and author, and prior versions become read-only but resolvable by version ID.
Needs/Actions-Only Data Capture (No Diagnoses)
Given I enter an accommodation in a free-text needs field, When the value contains diagnosis terms from the system-maintained blocklist, Then the save is blocked with a validation message instructing action-oriented phrasing and the record is not stored. Given I create a badge in mobility or sensory category, When saving, Then at least one needed_action/support_required field is present and non-empty; otherwise the save is rejected with 422. Given an API client submits text exceeding the max length or containing PHI patterns (e.g., ICD codes), When processing, Then the request is rejected with 422 and no data is stored.
Localization and Cross-Platform Label Consistency
Given the app locale is set to a supported language, When viewing badge labels and help text, Then localized strings are displayed; if a translation is missing, the English fallback is shown and a missing-key event is logged. Given mobile and web clients request the schema, When fetching, Then the same machine_key and schema_version are returned across platforms within 1 minute of publish. Given I switch locale, When refreshing a roster, Then badge labels and picklist values render in the new locale without requiring data migration.
Secure CRUD APIs with Field-Level Encryption and Key Management
Given I call POST/PUT on badges with a Confidential field, When the request is authorized with scope badges.write, Then the value is encrypted at rest with the active key (kid) and is only returned decrypted to roles with explicit field access; otherwise 403. Given the key rotation job runs, When rotation completes, Then all Confidential field ciphertext is rewrapped to the new active key without data loss and reads continue uninterrupted; the previous key is marked inactive after the configured grace period. Given an API client uses TLS below 1.2 or lacks required scopes, When calling any badges endpoint, Then the connection is refused or returns 401/403 and no data is changed.
Import/Export with Schema Validation and PII Safeguards
Given I run an import in dry-run mode with CSV/JSON, When columns/keys are unmapped or values violate field_type/sensitivity, Then a detailed report lists row and field errors and zero records are written. Given I run a committed import, When any row fails validation, Then the import aborts with no partial writes unless continue_on_error is enabled; valid rows are written and failures are reported. Given I export badges, When my role lacks export_confidential scope, Then Confidential fields are excluded or masked; exports include schema_version, are watermarked, and are audit-logged.
Role-Based Visibility, Shift Logic Integration, and Leakage Prevention
Given a Captain and a Peer view the same roster, When badges are rendered, Then the Captain sees full details per sensitivity and consent, the Peer sees only minimal indicators, and non-privileged users see nothing; attempts to access hidden fields return 403 or null. Given the shift assignment algorithm runs, When evaluating candidates, Then it reads only the minimal badge attributes necessary for matching and does not write badge data or derived sensitive info to non-privileged fields. Given public or non-privileged surfaces (e.g., Impact Board, public links, generic reports), When rendering, Then individual badge details are never displayed; only aggregate/anonymized indicators appear per policy.
Retention Policies and Consent Ledger Auditing
Given retention policies per sensitivity class are configured, When a badge reaches its retention end or consent is revoked, Then the system deletes or minimizes the data within the configured window and subsequent reads return 404 or redacted values. Given any change to a volunteer’s badge visibility comfort level, When saved, Then the Consent Ledger records volunteer, actor, timestamp, previous and new scope, and reason, and the entry is immutable and queryable by admins. Given a hard-delete operation, When executed, Then the deletion is propagated to replicas and backups within the configured period and is reflected in audit logs without exposing original content.

Resource Match

Forecasts accommodation demand from incoming prompts and suggests the resources to meet it—chairs, interpreters, quiet-area signage, or alternate assignments. Pings partner pools when supply is short, proposes swaps to keep commitments, and shows a readiness bar so Captains know when the plan is truly inclusive.

Requirements

Accommodation Demand Intake & Forecasting
"As a Captain, I want the system to automatically collect and forecast accommodation requests from signups and messages so that I can prepare resources ahead of time and avoid excluding anyone."
Description

Ingests accommodation needs from GiveCrew’s existing signup forms, event prompts, and message replies, normalizes them into structured demand (type, quantity, time window, location, criticality), and forecasts requirements per event and shift. Uses lightweight NLP plus rules to tag common needs (e.g., seating, ASL, quiet area, alternate assignment), deduplicates repeat requests, and rolls up lead-time projections so organizers can act before shortages occur. Integrates with event schedules, attendee rosters, and the mobile workflow to capture updates in the field and immediately refresh forecasts. Expected outcome is a reliable, up-to-date view of accommodation demand that reduces last-minute scrambles and missed commitments.

Acceptance Criteria
Normalize Accommodation Requests into Structured Demand
Given a new submission from signup forms, event prompts, or message replies, When it is received by the system, Then a demand record is created with fields: type (from controlled taxonomy), quantity (integer, default 1 if unspecified), time_window (start/end aligned to event/shift), location (resolved to event venue/area), and criticality (default Standard unless explicitly urgent). Given free-text like "need two chairs at 3pm during setup", When processed, Then demand.type = Seating, quantity = 2, time_window maps to the setup shift covering 3pm, location = event location, and a confidence score is stored. Given a submission with missing or unmappable elements, When processed, Then the record is flagged Needs Review with missing fields identified and excluded from forecast totals until resolved. Processing latency is <= 2 seconds p95 per submission and errors are logged with traceable source IDs.
Auto-Tag Common Needs via NLP + Rules
Given text containing synonyms for common needs (e.g., "ASL interpreter", "sign language", "need an interpreter"), When processed, Then the system tags demand.type = ASL with confidence ≥ 0.80. Given phrases for seating, quiet area, or alternate assignment, When processed, Then the system tags Seating, Quiet Area, or Alternate Assignment respectively with ≥ 95% precision and ≥ 90% recall on a curated phrase test set. Given confidence < 0.60 for any classification, When processed, Then the demand is labeled Uncategorized and routed to the Review queue with top 3 suggested tags. Given an admin-configured custom keyword-to-type mapping, When incoming text matches, Then the mapped type is applied with confidence = 1.0 and logged as rule-based.
Deduplicate Repeat Requests Across Channels
Given multiple requests from the same person (matched by email or phone) for the same event and need type within a 30-day window, When processed, Then they are consolidated into a single demand. When consolidating duplicates, Then quantity = maximum of observed quantities, time_window = union of overlapping windows, and notes retain the latest message; a merge history lists all sources. Given duplicate requests arriving via different channels (form, SMS, DM), When processed, Then only one demand contributes to forecast counts. Deduplication completes within 10 seconds of the latest related submission and exposes a deterministic merge key (event_id + person_id + need_type).
Forecast Demand per Event and Shift with Lead-Time Rollups
Given an event with defined shifts and normalized demands, When the forecast job runs on any change and hourly thereafter, Then per-shift aggregated demand totals are computed by need type and criticality. Given an event date, When forecasts are generated, Then lead-time rollups provide totals at T-14d, T-7d, T-48h, and T-24h snapshots for each need type. Given overlapping demands across shifts, When aggregating, Then quantities are attributed to the shift whose time_window overlaps most with the demand window; ties favor earlier shifts. Forecast outputs include a last_updated timestamp and are available via API and dashboard within 5 seconds of computation completion.
Realtime Forecast Refresh from Mobile Field Updates
Given a field user updates a demand (new request, change quantity, mark fulfilled) in the mobile workflow, When saved, Then the event/shift forecasts recalculate and refresh within 10 seconds, and the dashboard last_updated timestamp advances. Given a fulfillment update that drops forecasted availability below required demand for any type, When detected, Then a Shortage predicted flag is set for that type and shift with shortage quantity. Given an offline mobile update, When the device reconnects, Then queued changes sync and trigger the same recalculation behavior without duplication.
Integrate with Event Schedules and Attendee Rosters
Given a shift time change or a new shift added in the event schedule, When saved, Then associated demand time_windows are re-evaluated and re-attributed to the correct shift with no orphaned demands. Given an attendee who requested an accommodation cancels their participation, When the roster updates, Then their linked demand is marked Canceled and removed from forecast totals; the change is logged with actor and timestamp. Schedule/roster syncs propagate to forecasts within 30 seconds of the upstream change and are idempotent on retries.
Handle Ambiguous or Incomplete Requests
Given a request lacking sufficient information to determine type, quantity, or time_window, When processed, Then a draft demand is created with status Needs Clarification and it is excluded from forecast totals. Given a Needs Clarification demand, When viewed in the Review queue, Then the system suggests a follow-up prompt (e.g., ask for quantity or timing) and allows one-tap resolution to fill missing fields. All clarification actions and field edits are audit-logged (who, what, when), and unresolved items older than 24 hours appear in a prioritized escalation list.
Resource Catalog & Availability Sync
"As an Organizer, I want a searchable, accurate catalog of resources and their availability so that I can see what’s on hand and what must be sourced from partners."
Description

Defines a unified catalog of resource types (e.g., folding chairs, wheelchairs, ASL interpreters, quiet-area signage) with attributes like unit capacity, certifications, language, accessibility tags, travel radius, ownership (internal vs partner), availability windows, costs, and SLAs. Syncs real-time availability from internal inventory and partner pools via lightweight integrations or CSV uploads, and supports check-in/out to prevent double-booking. Ties resources to events and shifts in GiveCrew’s calendar so supply can be tracked against forecasted demand. Expected outcome is a clear, accurate view of available supply and constraints that enables dependable matching.

Acceptance Criteria
Create and Manage Resource Types with Attributes
Given I am an Org Admin in GiveCrew, When I create a new resource type with name, unit_capacity, certifications, languages, accessibility_tags, travel_radius_km, ownership, availability_windows, cost_per_unit, and sla_terms, Then the resource type is saved and visible in the catalog with all fields stored correctly. Given a resource type with the same name already exists in my org, When I attempt to create another with the same name, Then I receive a validation error and the new record is not created. Given a resource type exists, When I edit any attribute and save, Then the change is saved and effective for new allocations while existing allocations remain unchanged unless explicitly updated. Given a resource type is set to inactive, When I attempt to allocate it to an event or shift, Then it is excluded from eligible resources and cannot be allocated.
Real-Time Availability Sync via Partner API
Given an active partner integration with external_id mappings, When the partner updates availability and sends a webhook, Then the matching resource availability is updated within 60 seconds and last_sync_at reflects the update time. Given the partner sends the same update more than once, When the sync job processes it, Then the resulting availability is idempotent with no duplicate units or allocations created. Given the partner endpoint is unreachable during a scheduled sync, When the sync runs, Then sync_status is set to "sync_error", the previous availability remains unchanged, and a retry is scheduled within 5 minutes. Given a resource unit is retired in the partner system, When the next sync occurs, Then the corresponding unit in GiveCrew is marked inactive and becomes ineligible for allocation.
CSV Upload Sync with Mapping and Deduplication
Given a CSV with required columns [external_id, name, unit_capacity, ownership, availability_start, availability_end], When I upload and map columns, Then the system validates required fields and shows a preview with counts for to_create, to_update, and to_error. Given rows have missing required fields or invalid values, When I attempt the import, Then those rows are rejected with row-level error messages while valid rows are created or updated. Given duplicate rows by external_id exist in the CSV, When I import, Then only one record per external_id is processed and duplicates are reported as errors. Given an updated CSV contains existing external_ids, When I import, Then matching records are updated in place without creating duplicates and last_sync_at is updated.
Check-In/Check-Out and Double-Booking Prevention
Given a resource unit is available, When I check it out to Event A Shift 1 for 10:00–12:00, Then its status is "allocated" for that window and it is not eligible for other allocations that overlap that window. Given two concurrent allocation requests target the same unit for overlapping times, When both are submitted, Then only the first succeeds and the second returns a 409 conflict with no double-booking. Given an allocation ends or I perform an early check-in, When the check-in is recorded, Then the unit becomes available immediately for subsequent allocations starting after the check-in timestamp. Given a unit is not checked in within 15 minutes after the planned end time, When the system evaluates allocations, Then the allocation is flagged as "overdue" in the allocations list until checked in.
Link Resources to Events/Shifts and Track Supply vs Demand
Given an event with shifts has a requirement of 50 folding chairs, When I allocate 30 chairs from internal inventory and 15 from a partner pool, Then the event shows Allocated=45, Required=50, and Shortage=5 for the selected time window. Given an event time is rescheduled to a new window, When the new time overlaps existing allocations, Then the system recalculates and flags any allocations now in conflict and releases allocations outside the new window. Given an allocation is removed from an event, When I confirm removal, Then the released units immediately reflect as available in the catalog and the event’s shortage updates accordingly.
Ownership, Costs, SLAs, and Eligibility Constraints
Given a resource has ownership=partner and cost_per_unit=25, When 4 units are allocated to an event, Then the event shows a resource cost subtotal of 100 for that resource. Given an event location is 30 km from the resource base and the resource travel_radius_km=20, When attempting to allocate, Then the resource is not eligible for selection. Given a shift requires language=ASL and certification=RID, When filtering resources for allocation, Then only resources with matching language and certification tags are shown as eligible. Given an SLA requires 24-hour lead time for booking interpreters, When an allocation attempt is made inside the SLA window, Then the allocation is blocked with an SLA violation message and no allocation is created.
Constraint-Aware Matching Engine
"As a Captain, I want the system to suggest the best resource mix under our constraints so that we can cover needs fairly and efficiently without manual spreadsheet juggling."
Description

Matches forecasted demand to available resources using constraints such as time overlaps, quantity, skills/certifications, language, ADA compliance, travel distance, budget caps, and assignment conflicts. Produces ranked suggestions with rationale, including partial fills, alternates (e.g., reassign a bilingual volunteer), and scenario simulations when supply is short. On acceptance, auto-creates reservations, assignments, and tasks within GiveCrew to operationalize the plan. Expected outcome is a set of actionable, optimized suggestions that maximize inclusion while respecting real-world limitations.

Acceptance Criteria
Time Overlaps, Conflict Resolution, and Ranked Suggestions
Given an event with three demand blocks (10:00–11:00, 10:30–12:00, 12:00–13:00) and a resource pool where some availabilities overlap And maxSuggestions=5 When the matching engine runs Then no resource is assigned to two suggestions that overlap in time by more than 0 minutes And the engine returns a ranked list (1..5) of suggestions with a rationale array per suggestion listing satisfied/unsatisfied constraints and weights And existing confirmed assignments are treated as hard constraints and not violated in final suggestions And at least one swap proposal is generated for each detected conflict that preserves required skills and keeps deltaTravelMiles per affected resource <= 5 And execution completes in <= 2 seconds for datasets up to 500 resources
Quantity Matching with Partial Fills and Alternates
Given a demand for 50 chairs from 14:00–16:00 And inventory has 30 on-site chairs and 10 off-site chairs available with 1 hour travel time When the matching engine runs Then it proposes at least one partial-fill suggestion covering 40/50 with gap=10 clearly indicated And no suggestion allocates more than available quantities And the rationale for the partial-fill suggestion includes reason="inventoryShortfall" and lists alternates (e.g., partner request for 10) marked externalRequired=true
Skills/Certifications and Language Compliance
Given a demand for an interpreter requiring ASL and Spanish fluency plus certification=StateLevelX for 11:00–12:00 And the resource pool includes interpreters with varying languages/certifications and a bilingual volunteer currently assigned as greeter When the matching engine runs Then all top-ranked direct matches satisfy all required skills/certifications/languages And suggestions that do not fully satisfy requirements are labeled alternate with unmetConstraints listed And the engine proposes a swap that reassigns the bilingual volunteer to interpreter if greeter coverage remains >= 80% and all interpreter requirements are met
ADA and Inclusion Resource Coverage
Given an event marked with inclusion requirements: 2 ASL interpreters, 1 wheelchair-accessible check-in lead, and 3 quiet-area signage kits When the matching engine runs Then each suggestion returns inclusionCoveragePercent and a gaps list with type and quantityMissing And at least one suggestion achieves inclusionCoveragePercent >= 95% And any unmet ADA items are flagged with recommended mitigations (e.g., alternate assignments or signage substitutions) in the rationale
Travel Distance and Budget Cap Respect
Given per-resource maxTravelMiles=15 and event budgetCapUSD=500 And resources have home coordinates and per-assignment cost estimates When the matching engine runs Then no assignment in any suggestion exceeds maxTravelMiles unless flagged requiresOverride=true with justification And totalEstimatedCostUSD for each suggestion <= budgetCapUSD unless flagged requiresOverride=true with cheaper alternatives ranked above when available
Scenario Simulations Under Short Supply
Given supply cannot meet 100% of the inclusion requirements When the matching engine runs in simulation mode Then it generates at least three scenarios labeled "maximize inclusion", "minimize cost", and "balance risk" And each scenario includes scores: inclusionCoveragePercent, totalCostUSD, and riskScore(0–100) And scenarios are ranked by a composite score and are exportable as JSON
Operationalization on Acceptance (Reservations/Assignments/Tasks)
Given a user accepts suggestion #1 When the engine processes the acceptance Then it creates reservations, assignments, and tasks in GiveCrew within 2 seconds And all created records link to demandId and suggestionId and are visible in the activity log And the operation is idempotent so repeated acceptance does not create duplicates And on partial failure the system rolls back and surfaces an error with correlationId and no orphaned records remain
Partner Pool Outreach & Escalation
"As a Partnerships Lead, I want automatic, respectful outreach to partner pools when we’re short so that we can fill gaps quickly without endless back-and-forth."
Description

Automatically contacts partner pools when internal supply cannot meet forecasted demand, sending structured requests via email/SMS/push with essential details (what, when, where, quantity, qualifications, stipend). Provides one-tap accept/decline flows for partners, holds and confirmations, SLA timers, and configurable escalation to secondary partners or alternates if responses lag. Deduplicates outreach to avoid spamming and logs commitments back to availability for accurate readiness calculations. Expected outcome is faster fill rates and preserved partner goodwill through clear, respectful coordination.

Acceptance Criteria
Auto-Trigger Outreach When Forecast Exceeds Internal Supply
Given a forecasted demand Q for resource type T at event E in window W and internal supply S < Q And a configured outreach.createWithin SLA of 2 minutes When Resource Match computes allocations Then the system creates an outreach request R for deficit D = Q - S linked to E, T, and W within 2 minutes And no outreach is created if S >= Q And R respects partner minQty constraints by batching as needed And R records the configuration snapshot used (SLAs, pools, preferences) for auditability
Structured Multi-Channel Request Payload and Delivery
Given partner pool P with contact preferences and supported channels (email, SMS, push) And an outreach request R with fields (what, when [start/end + timezone], where, quantity, qualifications, stipend, response deadline, requestId) When R is sent Then each partner in P receives a message on their preferred channel(s) containing all required fields and one-tap Accept/Decline URLs And message delivery status is tracked per channel (queued, sent, delivered, failed) And transient failures are retried up to 3 times with exponential backoff starting at 1 minute And hard bounces or invalid numbers are marked and excluded from further attempts for R
One-Tap Partner Response Flow with Holds and Confirmations
Given a partner opens the one-tap link for request R before the response deadline When the partner taps Accept and specifies quantity q (1 ≤ q ≤ requested remaining) optionally adding notes Then the system places a Hold for q with a configured hold window (e.g., 30 minutes) and shows a Confirm action And on Confirm within the hold window, the commitment is finalized and the partner receives a confirmation receipt And if the hold expires without confirmation, q is automatically released and the deficit reopens And if the partner taps Decline, a decline reason is captured (if provided) and no further reminders are sent to that partner for R And one-tap links are single-use, signed, and expire at or before the response deadline
SLA Timers and Configurable Escalation to Secondary Pools
Given SLAs are configured: acknowledgeSLA = 15 minutes, fillSLA = 2 hours, and escalation rules to secondary pools P2 and alternates When no partner in the primary pool acknowledges R within 15 minutes Then the system escalates R to P2 and notifies the Captain with the escalated deficit And when a partial deficit remains at 2 hours, the system escalates to alternates (e.g., compatible resource types) and/or proposes swaps And partners who Declined or have Active Holds are excluded from escalation outreach for R And all SLA checkpoints and escalations are timestamped and visible in the request timeline
Outreach Deduplication and Partner Preference Compliance
Given a partner X may exist in multiple pools and has channel preferences, quiet hours, and opt-out settings When issuing outreach for request R Then X receives at most one outreach per request across all channels within a 24-hour cooldown And channel selection honors X's preferences and quiet hours; if all preferred channels are disallowed, R is queued until the window opens And retries do not create duplicate messages or duplicate outreach records And duplicate inbound responses for the same requestId and partnerId are idempotent and do not alter state more than once
Commitment Logging and Readiness Recalculation
Given partner Y confirms q units for request R linked to event E When the confirmation is recorded Then the availability ledger is updated and q units are assigned to specific shifts/slots where applicable And the outstanding deficit is reduced by q and the readiness bar recalculates within 60 seconds And the commitment appears on the Impact Board and in partner Y's profile with status Confirmed And cancelations or reductions adjust the ledger and readiness within 60 seconds and trigger re-outreach if a deficit reopens
Audit Trail, Security, and Error Handling
Given any outreach, response, escalation, retry, or cancellation related to request R occurs When the event is processed Then an immutable audit log entry is written with actor/partner, action, timestamps, requestId, correlationId, and redacted payload And all public response links are signed and time-limited; invalid or expired links show a friendly error and do not change state And delivery failures and SLA breaches raise alerts in the Captain dashboard with actionable next steps And admin users can export a CSV/JSON report of R's timeline and outcomes
Captain Review & Swap Proposals
"As a Captain, I want to quickly review and tweak suggested matches on my phone so that I can keep the plan inclusive even when circumstances change."
Description

Delivers a mobile-first review screen where Captains can accept or edit suggested matches, adjust quantities, and propose swaps across shifts or volunteers to maintain commitments. Highlights conflicts, warns on undercoverage, and records decision rationale and change history for accountability. Approved actions write back to assignments, tasks, and inventory reservations in GiveCrew. Expected outcome is a transparent, low-friction control point that keeps the plan realistic and inclusive under changing conditions.

Acceptance Criteria
Accept Suggested Matches Without Edits
Given I am a Captain on the mobile Review screen with system-suggested matches pending When I tap Approve on a suggested match without making edits Then the match is committed to assignments, tasks, and inventory reservations atomically And a success confirmation appears within 2 seconds And the approved item is removed from the pending list And reopening the event shows the committed records updated accordingly
Adjust Resource Quantities
Given a suggested resource allocation with an editable quantity When I increase or decrease the quantity Then any undercoverage warning updates immediately to reflect the new quantity And pending inventory reservation deltas are recalculated in draft until approval When I attempt to set a quantity below 0 or above available capacity Then the input is rejected with an inline validation message explaining the valid range When I save the edit Then the new quantity is stored for approval and captured for change history
Propose Cross-Shift Volunteer Swap
Given two shifts and at least one assigned volunteer is eligible for both When I select a volunteer and propose a swap from Shift A to Shift B Then the system validates schedule conflicts, double-booking, and role eligibility rules And shows a before/after preview of coverage impact for both shifts And flags any blocking conflicts that must be resolved before approval When I submit the proposal Then a rationale is required and the proposal is added to the pending changes queue
Conflict Detection and Undercoverage Warnings
Given I perform an action that creates a hard conflict (double-booking a person or negative inventory) or results in undercoverage versus configured minimums When I review the pending change Then the specific conflict or warning is highlighted inline with the affected items And hard conflicts block approval until resolved And undercoverage can be approved only after acknowledging the warning and entering a decision rationale
Decision Rationale and Change History
Given I edit a suggestion, override a warning, or approve a proposal When I attempt to save Then a non-empty decision rationale is required for overrides and approvals And the audit log records actor, timestamp, entity, action, and before/after values with the rationale And the change history is visible from the Review screen for accountability
Approved Actions Write Back Atomically
Given I have one or more pending edits and proposals When I tap Approve All or Approve on an individual item Then the system applies updates to assignments, tasks, and inventory reservations as a single transaction per approval And if any update fails, all related changes are rolled back and an error identifies the failing step And on success, downstream lists reflect the updates within 5 seconds and a confirmation shows the number of items updated
Mobile-First Performance and Accessibility
Given a smartphone with a 360px viewport and a 3G network profile When I open the Captain Review screen Then the first contentful paint occurs within 2 seconds and the screen is interactive without horizontal scrolling And all touch targets are at least 44x44 px and labeled for screen readers And color contrast meets WCAG 2.1 AA for text and interactive elements
Inclusive Readiness Bar UI
"As an Organizer, I want a clear readiness indicator that shows what’s covered and what’s missing so that I know exactly how close we are to being inclusive-ready."
Description

Displays a real-time readiness bar per event and shift that weights critical accommodations, shows percent of demand covered, and reflects buffers and SLAs. Color-coded states and tooltips explain what’s missing, while drill-down links jump to the specific gaps or pending partner confirmations. The bar blocks marking an event as ‘Ready’ until minimum inclusive criteria are met. Expected outcome is a shared, at-a-glance truth for when plans are truly inclusive and what actions remain.

Acceptance Criteria
Weighted Coverage Calculation Reflects Demand, Buffers, and SLAs
Given an event has forecasted demand per accommodation type with per-type weights, buffer percentages, and SLA confirmation deadlines When resources are assigned with statuses (confirmed|pending|unavailable) Then required_with_buffer = ceil(forecast * (1 + buffer%)) per type And fulfilled = count of confirmed resources per type (pending do not count toward fulfilled) And readiness_percent = round_to_whole(100 * sum_over_types(weight * min(fulfilled / required_with_buffer, 1)) / sum_over_types(weight)) And any type past its SLA confirmation deadline with fulfilled < required_with_buffer displays an SLA-breach flag in the bar tooltip
Color States and Explanatory Tooltips Map to Readiness and Critical Gaps
Given readiness_percent is computed and per-type coverage is known When readiness_percent < 70% OR any critical type has fulfilled < required_with_buffer Then the bar state is Red and the tooltip lists the blocking critical gaps by type with counts (e.g., “Interpreters: 2 missing”), plus a “View all gaps” link When 70% <= readiness_percent <= 89% AND all critical types have fulfilled >= required_with_buffer Then the bar state is Amber and the tooltip lists the top 3 remaining gaps with counts and ETAs from pending confirmations When readiness_percent >= 90% AND all minimum inclusive criteria are met (all critical types at 100% of required_with_buffer, no SLA breaches) Then the bar state is Green and the tooltip indicates “Minimum inclusive criteria met”
Drill-Down Links to Gaps and Pending Confirmations
Given a user opens the readiness bar tooltip When the user selects a “X missing” gap link for a type Then the app navigates to the Resource Planner filtered to the selected event/shift and type within 2 seconds p95 (4 seconds max), scrolled to the first deficit row When the user selects a “Y pending partner confirmations” link Then the app opens the Partner panel filtered to pending confirmations for the event/shift/type within 2 seconds p95 (4 seconds max) And all deep links include source=readiness_bar for analytics and enforce permissions (unauthorized users see an error toast and no navigation)
Block Marking Event Ready Until Minimum Inclusive Criteria Met
Given the definition of minimum inclusive criteria (all critical types at 100% of required_with_buffer confirmed; no SLA-breach flags outstanding) When a user attempts to mark an event or shift as Ready while criteria are not met Then the Ready control is disabled and/or presents a modal that enumerates each unmet criterion with counts and provides shortcuts to resolve (e.g., “Add 2 interpreters”) When all criteria are met Then the Ready control is enabled, the action succeeds, and an audit log records user, timestamp, and readiness_percent at confirmation
Real-Time Readiness Updates on Assignment and Forecast Changes
Given the readiness bar is visible for an event/shift When a resource assignment is confirmed, canceled, or moved; a partner confirmation arrives; or demand/buffer configuration changes Then the readiness bar recalculates and updates within 2 seconds p95 (4 seconds max) without page refresh, showing a loading shimmer for any delay > 300 ms (<= 500 ms duration) And Event-level and Shift-level bars remain consistent within the same update cycle And on transient failures the UI shows a non-blocking error and retries with exponential backoff up to 3 attempts
Accessible and Non-Color-Dependent Readiness Bar
Given users may rely on assistive technologies or have color-vision deficiencies When interacting with the readiness bar and tooltip Then the bar meets WCAG 2.1 AA contrast; includes text labels for percent and state; does not rely on color alone (adds iconography/patterns) And the bar and tooltip are fully keyboard operable (Tab/Shift+Tab to focus, Enter/Space to activate, Esc to dismiss) And the tooltip content is exposed to screen readers via ARIA with appropriate roles and descriptions, and focus management returns to the trigger on close
Impact Board Integration & Reporting
"As a Director, I want inclusion metrics on the Impact Board so that I can show stakeholders our progress and target funding where it will improve access the most."
Description

Feeds the Impact Board with inclusion metrics such as accommodations requested vs. met, time-to-fill, partner fill rate, budget utilization, and unmet needs with reasons. Supports privacy-safe redaction of sensitive details, exportable summaries for funders, and filters by event, date range, and location. Expected outcome is visible, credible reporting that demonstrates progress and guides future investment in inclusive resources.

Acceptance Criteria
Impact Board Displays Core Inclusion Metrics
- Given Resource Match data exists for the selected scope, when the Impact Board loads, then it displays: accommodations requested, accommodations met, requested_vs_met percentage, median (p50) and p90 time-to-fill, partner fill rate percentage, budget utilization percentage, and unmet needs count with reason breakdown. - Given no data exists for the selected scope, when the Impact Board loads, then metrics render with zero values and a No data indicator without errors. - Given new assignments are confirmed, when the Impact Board refreshes, then metrics update within 5 minutes and reflect the latest state. - Given canceled or duplicate requests, when metrics are calculated, then they are excluded from counts and durations. - Given requests with quantities, when counts are calculated, then counts are unit-based and partial fills are reflected as met units and remaining unmet units. - Given events span time zones, when time-based metrics are calculated, then they use the event's local time zone.
Filter by Event, Date Range, and Location
- Given the user selects one or more events, a date range, and one or more locations, when Apply is clicked, then all metrics and breakdowns recompute to include only records matching all selected filters. - Given no filters are selected, when the Impact Board loads, then it defaults to current month-to-date and all locations and events. - Given saved filters exist, when a saved filter is applied, then filter chips reflect the saved selections and metrics match the saved state. - Given a dataset up to 100,000 records, when filters are applied, then results render within 3 seconds at p95 and without timeouts.
Privacy-Safe Redaction and Role-Based Views
- Given privacy mode is enabled or the viewer has the Funder role, when viewing the Impact Board and exports, then no PII (names, emails, phones) or medical notes are displayed; only aggregated metrics and reason categories are shown. - Given any breakdown cell would represent fewer than 5 individuals or units, when rendering, then the value is suppressed and shown as <5 and included only in totals. - Given the Internal role with redaction toggled off, when viewing drill-downs, then PII fields are visible; when redaction is toggled on, then PII is masked immediately without page reload. - Given a redaction state change, when it occurs, then an audit log entry is recorded with user, timestamp, and scope. - Given privacy mode, when exporting, then only summary tables and charts are included; no row-level records are present in the files.
Exportable Summaries for Funders
- Given Funder role or privacy mode, when Export Summary is clicked, then PDF and CSV files are generated containing: selected filters, reporting period, metric definitions, values for requested vs met, median and p90 time-to-fill, partner fill rate, budget utilization, and unmet needs with reason breakdown. - Given the export is generated, when downloaded, then the PDF is accessibility tagged (title, language, alt text for charts) and the CSV is UTF-8 encoded with a header row. - Given the export is generated, when totals are compared to on-screen values for the same filters, then they match exactly and include an ISO 8601 timestamp and application version. - Given a 12-month reporting window, when export is requested, then the export completes within 60 seconds; if longer, a progress indicator is shown and the export completes within 5 minutes without data loss.
Partner Fill Rate Attribution Accuracy
- Given a resource unit is fulfilled by a partner pool, when the assignment is confirmed, then the unit is attributed to that partner and included in the partner fill rate calculation: partner_fill_rate = partner_filled_units / total_filled_units (for the selected scope). - Given multiple partners fulfill units within a single request, when attribution is calculated, then units are attributed by quantity to each contributing partner. - Given a partner declines or times out, when reasons are recorded, then these events do not increase partner_filled_units but do contribute to unmet reasons if the units remain unfilled. - Given a swap occurs before the event, when final fulfillment is recorded, then attribution reflects the final fulfilling partner. - Given corrections are applied to source data, when a resync occurs, then partner fill rate recomputes and an audit entry of recalculation is stored.
Time-to-Fill and Budget Utilization Calculations
- Given a request is created at T0 and the final unit is confirmed at T1, when time-to-fill per request is calculated, then duration = T1 - T0 in the event's local time zone; canceled requests prior to any fill are excluded; reopened requests reset T0 at reopen time. - Given partial fills across timestamps, when time-to-fill per unit is calculated for percentile charts, then each unit uses its own confirmation time; per-request summaries use the final unit confirmation time. - Given budget allocations and spend entries exist for the selected scope, when budget utilization is calculated, then utilization = sum(spend) / sum(allocated); values over 100% show an overrun indicator. - Given filters change, when utilization is recalculated, then only spend and allocations within the filtered scope are included. - Given allocation data is missing for the scope, when utilization is displayed, then it shows N/A with a warning indicator and is omitted from export totals while noted in the export footnotes.
Unmet Needs with Reasons Classification
- Given a request is not fully filled by event start, when unmet is calculated, then unfilled units are counted as unmet and categorized by a controlled reason list (e.g., no supply, late notice, budget cap, accessibility conflict). - Given multiple reasons apply to unfilled units, when recorded, then reasons can be multi-selected and units are apportioned to the selected reasons; if not apportioned, they are categorized as Multiple. - Given the reasons taxonomy changes, when viewing historical data, then legacy reasons remain mapped to stable categories and are labeled as legacy. - Given filters are applied, when viewing the reasons breakdown, then counts and percentages update and the sum of reasons equals the total unmet units in scope. - Given privacy suppression thresholds, when a reason count is less than 5, then it is suppressed or combined into Other per privacy rules.

Standby Ladder

Builds a pre-ranked bench of opt-in backups for each phonebank. As T-10 approaches, SeatSaver Auto-Relay escalates invites through the ladder until every seat is filled—no captain juggling required. Captains can reorder priorities with one tap and see live fill status. Keeps calling teams at full strength while honoring fairness and partner attribution.

Requirements

Standby Opt-in and Preferences Capture
"As a volunteer, I want to opt in as a standby and set my preferences so that I only get invited to shifts that fit my schedule and interests."
Description

Enable volunteers to explicitly opt in as standby backups for specific phonebanks via mobile-friendly flows (event signup, QR, SMS keyword, or link) that record consent, preferred contact channel, availability windows, shift types, max outreach frequency, language, and partner affiliation. Store preferences on the GiveCrew contact profile, deduplicate against existing records, and enforce consent and do-not-contact flags across all outreach. Provide lightweight validation (e.g., phone/email verification) and immediate confirmation. Expose an organizer-facing view to see who is in the standby pool per event, filtered by preference fit. This expands the eligible bench while ensuring outreach relevance, compliance, and smooth integration with existing GiveCrew profiles, events, and messaging.

Acceptance Criteria
Multi-Channel Standby Opt-In Flow
Given a volunteer accesses the standby opt-in via event signup page, QR code, SMS keyword, or direct link When they provide minimum required info (name and at least one contact: mobile or email) and check consent Then a standby enrollment is created for the selected phonebank event with standby_opt_in=true and a UTC timestamp And the on-screen confirmation renders within 2 seconds and a confirmation message is delivered via the chosen channel within 10 seconds And the form is mobile-friendly (WCAG 2.1 AA) and loads within 3 seconds on a 3G connection
Preference Capture and Profile Storage
Given the opt-in form When the volunteer supplies preferred contact channel, availability windows (day/time with timezone), shift types, max outreach frequency per week, language, and partner affiliation Then all values are validated, normalized, and saved on the GiveCrew contact profile And existing profiles are updated (last-write-wins) with an audit log entry, without losing historical values And missing/invalid fields trigger inline errors that block submission until corrected
Consent and Do-Not-Contact Enforcement
Given the volunteer does not provide explicit consent When they attempt to submit Then no contact/profile changes or enrollments are created and a message explains consent is required Given a contact has do_not_contact=true When they attempt standby opt-in Then enrollment is blocked and a link to manage preferences is presented Given outreach is assembled for the standby pool When recipients are generated Then only contacts with consent=true, verified for the chosen channel, and under their max outreach frequency cap in the past 7 days are included
Deduplication and Idempotent Enrollment
Given multiple opt-in attempts by the same person across channels When phone (E.164) or email (RFC 5322) matches an existing profile Then no new contact is created and a single standby enrollment is upserted for the event Given two submissions occur within 5 minutes for the same person and event When processed concurrently Then exactly one enrollment record exists and preference updates are merged deterministically Given only ambiguous matches (e.g., same name, no matching contact info) are found When processed Then a new contact is created and flagged for merge review
Lightweight Phone/Email Verification and Immediate Confirmation
Given a mobile number is entered When verification is requested Then a 6-digit OTP SMS is sent and verification succeeds only with the correct code within 10 minutes and at most 5 attempts Given an email is entered When the verification link is sent Then clicking the link within 24 hours marks the email verified Given verification fails or is pending When enrollment is confirmed Then enrollment is created, the unverified channel is excluded from outreach until verified, and the confirmation screen prompts verification
Organizer Standby Pool View Filtered by Preference Fit
Given an organizer opens an event’s standby pool view When filters for event time window (in event timezone), shift type, language, channel verified, partner affiliation, and remaining frequency quota are applied Then the list updates within 2 seconds to show only matching volunteers and displays total/filtered counts Given an organizer opens a volunteer detail from the list When viewing the profile Then captured preferences, verification status, consent status, partner attribution, and last outreach timestamp are visible Given a volunteer updates preferences When the organizer refreshes the pool view Then changes are reflected within 60 seconds
Ladder Generation and Ranking Logic
"As a captain, I want the system to pre-rank backups fairly and accurately so that my phonebank stays staffed without manual juggling."
Description

Automatically assemble and score a pre-ranked ladder of standby volunteers for each phonebank using transparent, configurable criteria including reliability (show-up rate), recency of participation, prior role fit, time zone and proximity, language skills, preference match, and partner fairness quotas. Compute the ladder at event creation and re-evaluate at defined checkpoints (e.g., T-24, T-2) and on captain edits. Ensure deterministic ordering with clear tiebreakers and surface the reason codes for each rank to build trust. Support manual overrides while preserving audit trails and guardrails (e.g., cannot violate do-not-contact or partner caps). Provide an internal API to fetch, page, and update ladder entries for the UI and Auto-Relay engine.

Acceptance Criteria
Initial Ladder Build at Event Creation
Given a newly created phonebank event with published settings and an eligible volunteer pool, When the ladder generation job runs at event creation, Then the system assembles a standby ladder sized to event capacity plus the configured overfill buffer and persists snapshot version 1 for the event. Given configured factor weights for reliability, recency, prior role fit, timezone/proximity, language, preference match, and partner fairness, When scoring candidates, Then each candidate receives a normalized total score computed only from enabled factors and any candidate failing a configured disqualifier (e.g., do-not-contact, required language, time unavailability) is excluded prior to ranking. Given identical event data, configuration, and eligible pool, When the ladder is generated twice, Then the resulting order, scores, and reason codes are identical. Given a candidate has missing data for a non-required factor, When scoring, Then that factor contributes zero with a "no data" note in reason codes and the candidate remains eligible if not disqualified.
Scheduled Re-Evaluation at T-24/T-2 and on Captain Edits
Given an upcoming event, When the clock reaches T-24h and T-2h before start time, Then the ladder is recomputed using current data and configuration and a new snapshot version is stored with a diff summary of rank changes. Given a captain edits event settings that affect eligibility or priorities (e.g., required language, role fit, partner targets), When the edit is saved, Then the ladder recomputes within 60 seconds and persists a new snapshot linked to the edit audit record. Given a recomputation where no candidate scores change, When the job completes, Then ranks are preserved exactly and a "no changes" audit entry is recorded. Given a recomputation where some scores change, When the job completes, Then updated ranks and reason codes reflect the new factor contributions and include the recompute timestamp.
Deterministic Tie-Breaking Order
Given two or more candidates with equal total score, When ordering the ladder, Then the following tiebreakers are applied in strict sequence until the tie is broken: higher reliability, more recent participation, higher prior role fit, closer timezone/proximity, language match, higher preference match, greater partner fairness need, earlier last-invite timestamp, lower unique member ID. Given multiple tie levels, When applying tiebreakers, Then the first criterion that separates candidates determines order and the final order is stable across runs with identical inputs. Given a tie persists after all tiebreakers, When ordering, Then order by lower unique member ID and append "final tiebreaker: memberId" to reason codes.
Transparent Reason Codes per Rank
Given a generated ladder, When fetching any ladder entry, Then the entry includes reasonCodes listing top contributing factors with weights and any exclusions evaluated in human-readable phrases. Given a candidate excluded due to a hard rule (e.g., do-not-contact or required language), When inspecting reasonCodes, Then the first reason is the exclusion rule identifier with a short description. Given factor weights change between snapshot versions, When comparing an entry across versions, Then reasonCodes reflect updated weights and include a configVersion and configChanged=true flag. Given a captain requests an explanation for a candidate’s rank, When the API is called, Then it returns factorBreakdown including factor name, input values, normalized factor score, weight applied, and net contribution.
Manual Overrides with Guardrails and Audit
Given a captain moves a candidate up or down the ladder, When the override is submitted, Then the system applies the new rank, marks the entry as overridden, and writes an audit record with user, timestamp, old rank, new rank, and reason. Given an override would violate do-not-contact, partner caps/quotas, or hard constraints (e.g., required language, time availability), When the override is submitted, Then the system blocks the change, returns an error identifying the violated guardrail, and leaves the ladder unchanged. Given a previously overridden candidate whose computed score now surpasses the manual position, When a recomputation occurs, Then the override position is preserved until explicitly lifted by the captain and an "override active" flag is shown. Given a captain lifts an override on a candidate, When saved, Then subsequent recomputations place the candidate by computed score and the audit trail retains all prior overrides.
Internal API: Fetching, Paging, and Updating Ladder
Given a service client with valid authorization scope, When calling GET /internal/ladders/{eventId}?page=1&pageSize=50&filter=eligible&sort=rank, Then the API returns 50 entries, totalCount, snapshotVersion, and for each entry: memberId, rank, score, factorBreakdown, reasonCodes, partnerId, overrideFlag, dncFlag, and updatedAt. Given more than 200 entries, When paginating, Then pageSize is capped at 200 and cursor-based next/prev tokens are returned providing stable ordering by rank. Given a request without required authorization or scope, When calling any ladder endpoint, Then the API returns 401 or 403 with no ladder data. Given a valid request to POST /internal/ladders/{eventId}/entries/{memberId}/override with a newRank, When processed, Then the API updates the ladder, returns the updated entry, and emits an event "ladder.override.applied". Given a ladder of 1,000 entries, When fetching pageSize=100, Then p95 latency is <= 400ms and p99 latency is <= 800ms in the staging environment.
Partner Fairness Quotas and Caps Compliance
Given partner fairness quotas configured (e.g., max percent or seat cap per partner), When generating or recomputing the ladder, Then the top K ranks for event capacity K do not exceed configured caps and fairness adjustments are applied as needed. Given equal-scoring candidates across partners, When applying fairness, Then candidates from underrepresented partners are ranked ahead until targets are met and reasonCodes include a fairness-adjustment with partner and delta. Given no partner quotas are configured, When generating the ladder, Then fairness logic is not applied and reasonCodes contain no fairness entries. Given a captain attempts an override that would cause a partner cap violation within the top K, When the override is submitted, Then the change is blocked with a partner-cap violation message and no rank changes occur.
SeatSaver Auto-Relay Engine
"As a captain, I want invites to escalate automatically until every seat is filled so that I don’t have to spend time chasing backups."
Description

Drive automated, escalating outreach to the ranked standby ladder beginning at configurable time thresholds (e.g., starting at T-10 hours) to fill open seats. Send invites in controlled waves, hold seats for a short acceptance window, and advance down the ladder on decline or timeout until capacity is met. Respect quiet hours, contact frequency caps, and channel preferences; prevent double-booking across overlapping events; and synchronize confirmations back to the event roster in real time. Provide idempotent processing, rate limiting, retries, and audit logs for each outreach decision. Allow per-event configuration of wave size, timeout duration, maximum outreach depth, and stop conditions. Integrate with GiveCrew messaging providers and update the Impact Board when seats are filled.

Acceptance Criteria
Wave-Based Outreach Escalation from T-10
Given an event with 6 open seats and configuration: startThreshold = T-10h, waveSize = 3, waveCooldown = 5m, maxDepth = 12, stopCondition = "CapacityMet" When current time reaches T-10h before event start Then the engine sends exactly 3 invites to the top 3 ranked, eligible standby contacts not yet contacted for this event And no invites are sent before T-10h And the next wave is scheduled 5 minutes after the prior wave is sent or immediately upon all prior-wave responses being received, whichever occurs first And waves continue until 6 seats are accepted or maxDepth (12 distinct contacts) is reached or stopCondition is met
Seat Hold Window and Ladder Advancement
Given timeoutMinutes = 10 and openSeats = 2 When an invite is sent to a contact Then 1 seat is held for that contact for 10 minutes And if the contact accepts within 10 minutes, the seat is marked confirmed and removed from the ladder And if the contact declines or the 10-minute window expires, the hold is released and the next ranked, eligible standby is invited within 30 seconds And no more than openSeats concurrent holds exist for the event at any time
Quiet Hours and Contact Frequency Caps
Given quietHours = 21:00–08:00 local to the contact and contactCap = 2 outreach attempts per rolling 7 days per person When a wave would send an invite at 22:15 local or would exceed the per-person cap Then the engine defers that invite to 08:00 local or skips the contact and advances to the next eligible standby And no outreach is sent to contacts flagged DND or unsubscribed And deferred invites are executed within 2 minutes of quietHours ending, subject to rate limits
Channel Preferences and Fallback Respect
Given a contact with channel preferences [SMS, Email], language = Spanish, and valid opt-in on both When the engine sends an invite Then the invite is sent via SMS in Spanish first And if SMS returns a hard failure or no delivery acknowledgment is received within 60 seconds due to throttling, the engine sends the same invite via Email within the next 60 seconds, respecting rate limits and caps And no outreach is sent via any channel the contact has not consented to
Double-Booking Prevention Across Overlapping Events
Given a contact is confirmed or seat-held for an event that overlaps by ≥15 minutes with the target event When assembling the eligible list for a wave Then the contact is excluded from outreach for the target event And if the contact attempts to accept an invite that would overlap, the acceptance is rejected and the next ranked, eligible standby is invited within 30 seconds And removals of holds on one event immediately re-evaluate eligibility for the other within 10 seconds
Real-Time Roster Sync and Impact Board Update
Given an invite is accepted When the engine records the acceptance Then the event roster shows the contact as confirmed within 5 seconds And the Impact Board reflects the updated filled seats and progress within 10 seconds And if the acceptance is withdrawn before event start, both roster and Impact Board revert within 10 seconds and the ladder advances to fill the seat
Idempotency, Rate Limiting, Retries, and Audit Logging
Given an idempotencyKey composed of eventId+contactId+waveId for outreach decisions When the same decision is retried due to a transient failure Then no duplicate invite is sent and the same outbound messageId is returned And outbound messages respect provider rate limits (e.g., 25 SMS/sec); excess is queued without loss And transient errors (HTTP 429/5xx) are retried up to 3 times with exponential backoff (1s, 2s, 4s) And an immutable audit log entry is written for each decision and state change with timestamp, actor, reason code, inputs, outputs, and correlationId, queryable by eventId and contactId
One-Tap Reprioritization UI
"As a captain, I want to reorder the ladder with one tap so that I can account for context the algorithm can’t see."
Description

Offer a mobile-first interface that lets captains reorder the ladder with a single tap (pin-to-top, drag-and-drop), apply quick filters (skills, partner, language), and temporarily exclude or prioritize individuals. Display compact profile context (reliability, last shift, partner tag, notes) and clearly indicate fairness rule impacts when overrides are made; require justification when bypassing guardrails. Provide undo, multi-select actions, and accessibility support. Synchronize changes instantly with the ranking service and Auto-Relay engine to ensure outreach follows the updated order.

Acceptance Criteria
One-Tap Pin and Drag Reorder Updates Ladder and Auto-Relay
Given a captain has the standby ladder visible on mobile When the captain taps Pin to Top on candidate X or drags X to position 1 Then the UI reorders within 100 ms and shows X at position 1 with a Pinned indicator And the ranking service receives the new order within 2 seconds And the Auto-Relay engine uses the updated order for the next outreach attempt within 2 seconds And an Undo option is available for 10 seconds to revert the change And the change is audit-logged with user ID, timestamp, and before/after positions
Quick Filters by Skill, Partner, and Language
Given the ladder list is open When the captain applies filters for Skill, Partner, and Language (any combination) Then only candidates matching all selected filters are displayed And the active filter chips show selected values and a visible Clear All control And clearing filters restores the unfiltered list within 500 ms And filtering does not alter the relative rank order among the visible candidates And selected filters persist during in-session navigation until cleared by the user
Temporary Prioritize and Exclude Flags
Given a captain selects one or more candidates When the captain applies Prioritize for 24h or Exclude for 24h Then affected candidates display a corresponding badge and move relative to rank per flag semantics And flags automatically expire after the selected duration, restoring the prior rank And the ranking service is updated within 2 seconds and Auto-Relay honors the flags on next outreach And a counter shows the number of active prioritized and excluded candidates
Fairness Guardrail Override With Justification
Given a fairness rule prevents moving candidate X above candidate Y When the captain attempts to pin or move X above Y Then a modal explains the impacted fairness rule(s) and projected attribution impact And the captain must enter a justification of at least 10 characters to proceed And upon confirmation the override is applied, synchronized within 2 seconds, and logged with rule IDs and justification And if no justification is provided the override is blocked and no changes are saved
Compact Profile Context in Ladder Rows
Given candidates are rendered in the ladder Then each row displays reliability score (0–100), last shift date, partner tag, and the first 80 characters of notes And missing fields show standardized placeholders (e.g., No notes) And tapping a row opens a quick-view with full notes without leaving the ladder And performance: first 20 rows render within 1 second on a 3G profile and scrolling maintains 60 fps on the reference device
Accessibility and One-Hand Mobile Usability
Given a captain uses assistive technologies or large text Then every actionable element has an accessible name, role, and state announced correctly And drag-and-drop has alternative controls (Move Up/Down and Pin actions) operable without gestures And touch targets are at least 44x44 px with 8 px spacing and focus order is logical And color contrast meets WCAG 2.1 AA and dynamic type up to 200% preserves critical info without truncation And status changes are conveyed via multiple modalities (not solely color or haptics)
Multi-Select Batch Actions With Undo
Given the ladder is open When the captain enters multi-select mode and selects two or more candidates Then batch actions include Pin, Unpin, Prioritize (duration options), and Exclude (duration options) And a confirmation displays the number of candidates to be affected before applying And updates are applied within 2 seconds, synchronized to ranking and Auto-Relay, and can be undone within 10 seconds as a single operation And partial failures report per-candidate errors while successful updates persist
Live Fill Status and Alerts
"As a captain, I want live fill status and proactive alerts so that I can intervene early if a phonebank is at risk of being understaffed."
Description

Present real-time fill status for each phonebank, including seats filled, pending holds, declines, timeouts, remaining open seats, and estimated time to fill based on current response rates. Show delivery and response telemetry (sent, delivered, clicked, accepted) and identify bottlenecks. Provide configurable alerts to captains via in-app notifications, SMS, or email when thresholds are met (e.g., projected underfill at T-2, outreach depth nearing limit). Feed seat-fill milestones to the Impact Board and support exportable summaries for partner reporting. Ensure privacy by redacting personal data where required.

Acceptance Criteria
Live Fill Status Dashboard
Given a scheduled phonebank with defined total seats and volunteers in accepted, hold, declined, and timeout states, when a captain opens the Live Fill Status view, then Seats Filled, Pending Holds, Declines, Timeouts, and Remaining Open Seats reflect the true underlying states at query time. Given any change to a volunteer’s state is committed (accept, decline, release hold, timeout), when the change occurs, then the dashboard updates within 5 seconds and displays a Last Updated timestamp in the captain’s timezone. Given Remaining Open Seats is computed as Total Seats minus Seats Filled minus Active Holds, when displayed, then the value is an integer and never negative. Given the event start time and timezone are configured, when showing the countdown, then the T- value reflects the event timezone and is accurate to the minute. Given connectivity is temporarily lost, when the view is open, then the UI indicates offline status and resumes live updates within 5 seconds after connectivity returns.
Estimated Time to Fill (ETTF)
Given at least 30 outreach events occurred in the last 60 minutes, when computing ETTF, then the estimate uses observed accept rate and send pace from the last 60 minutes and displays an ETA with a 90% confidence band. Given fewer than 30 recent outreach events, when computing ETTF, then display Not enough data and fall back to the organization’s baseline model with the model source labeled. Given historical validation on at least 100 prior phonebank fills, when ETTF accuracy is measured, then the median absolute percentage error is ≤ 20%. Given the projected start time is within 10 minutes and ETTF exceeds the start time, when displayed, then a Projected underfill flag appears with the expected seat shortfall. Given assumptions change (send pace or accept rate shifts by ≥ 20% over a 10-minute window), when recalculating, then the ETTF and confidence band update within 10 seconds.
Outreach Telemetry and Bottleneck Identification
Given ladder outreach is active across channels, when viewing telemetry, then counts for Sent, Delivered, Clicked, and Accepted are shown per channel and as an aggregate total. Given a person receives multiple touches across channels for the same phonebank, when counting Accepted and Declined, then unique persons are deduplicated at the phonebank level. Given at least 50 total Sent events, when the drop-off between consecutive stages exceeds 15 percentage points at any stage, then that stage is labeled as the bottleneck and visually highlighted. Given late telemetry events (arrival delay < 5 minutes), when received, then the funnel reconciles and updates within 5 seconds of ingestion. Given telemetry provider outages, when delivery status is unknown, then the Delivered count excludes unknowns and an Unknown segment is displayed with a tooltip explaining data latency.
Threshold Alerts to Captains (In-app, SMS, Email)
Given a captain has selected alert channels and thresholds (Projected underfill at T-2h, Outreach depth ≥ 3 waves, Response rate < 5%), when any threshold is met, then an alert is delivered via the selected channels within 60 seconds. Given multiple threshold triggers occur within a 15-minute window, when alerts are generated, then duplicate alerts are suppressed and one consolidated alert with updated metrics is sent. Given an alert contains an action link, when the captain taps/clicks it, then the app deep-links to the specific phonebank’s Live Fill Status screen. Given delivery failure on a channel is detected, when sending alerts, then retry up to 3 times with exponential backoff and fall back to another selected channel if available. Given a captain has opted out of SMS/email, when thresholds are met, then only an in-app notification is sent and no SMS/email is delivered.
Impact Board Milestone Feed
Given seat-fill milestones at 25%, 50%, 75%, and 100%, when each threshold is crossed, then an event is emitted to the Impact Board within 15 seconds containing phonebank ID, milestone reached, timestamp, and total seats. Given milestone thresholds are crossed multiple times due to cancellations and refills, when emitting events, then duplicate milestone events within a 5-minute window are suppressed and the highest milestone within the window is retained. Given privacy settings prohibit PII on public displays, when sending milestone events, then no personal data is included; only aggregated counts and anonymized IDs are transmitted. Given the Impact Board endpoint is temporarily unavailable, when an event fails to deliver, then it is queued and retried for up to 15 minutes with idempotent delivery semantics.
Exportable Partner Summary with Privacy Redaction
Given a partner user requests an export for a date range and selected phonebanks, when generated, then the CSV and PDF include total seats, filled, holds, declines, timeouts, ETTF snapshots, outreach funnel counts by channel, and alert occurrences, all scoped to the selected range. Given the partner lacks PII permission, when exporting, then volunteer identifiers are fully redacted and replaced with stable anonymous IDs; no names, emails, phone numbers, or exact timestamps are included, and timestamps are bucketed to 15-minute intervals. Given the partner has PII permission, when exporting, then only volunteers who have consented to data sharing include PII; all others remain anonymized, and a consent coverage percentage is shown. Given an export is ready, when delivery is prepared, then a signed download URL is produced that expires after 24 hours and the access is logged with user ID, timestamp, and IP address. Given differing timezones, when rendering the export, then all times are normalized to the partner’s configured timezone and the timezone label is included in headers.
Fairness and Partner Attribution Controls
"As a coalition partner, I want equitable distribution and clear attribution of filled seats so that my volunteers are represented and credited fairly."
Description

Implement rule-based fairness that honors partner allocations and attribution, including per-event caps, rotation among partners, and minimum representation targets when feasible. Tag each seat with attribution on acceptance and display partner mix in status views and reports. Provide configurable policies per coalition, transparent explanations when the system selects or skips someone for fairness reasons, and override workflows with audit trails. Ensure rules never override consent or compliance constraints and integrate with overall GiveCrew reporting to credit partners accurately.

Acceptance Criteria
Enforce Per-Event Partner Caps During Seat Filling
- Given per-event partner caps are configured for a coalition, When SeatSaver Auto-Relay assigns seats, Then no partner’s assigned seats exceed its cap for that event. - Given a partner’s cap is reached, When candidates from that partner are next in the ladder, Then they are skipped and a fairness reason with timestamp is logged on the candidate and seat. - Given open seats remain and at least one partner is under cap, When eligible candidates exist, Then invitations are sent to under-cap partners until seats are filled or all caps hit. - Given a captain attempts to assign a candidate that would exceed a cap, When they proceed, Then the system blocks the action unless an authorized override is recorded; if overridden, an audit entry is created.
Rotate Invitations Equitably Across Partners in Ladder Escalation
- Given multiple partners have eligible opt-in candidates, When sequential invites are sent, Then partner selection rotates round-robin weighted by remaining allocation so that no eligible partner is skipped twice in a row. - Given candidates tie on priority, When selecting the next invitee, Then the system applies the configured deterministic tie-breaker (e.g., least recently invited, then seeded random) and logs the applied rule. - Given a partner has zero remaining allocation or is at cap, When the rotation reaches that partner, Then it is skipped with reason logged until allocation is replenished.
Maintain Minimum Representation Targets When Feasible
- Given minimum representation targets are configured, When current assignments are below target for any partner/group and eligible candidates exist, Then the system prioritizes invites for under-target groups without violating hard caps or compliance. - Given no eligible candidates exist for an under-target group, When seat filling proceeds, Then the system records infeasibility with data (group, target, reason) and continues best-effort allocations. - Given rebalancing is enabled before cutoff, When under-target candidates opt in later, Then the system may swap out lowest-priority over-represented assignments if acceptance can be confirmed before cutoff, with notifications and logs for both parties.
Tag Seats with Partner Attribution on Acceptance
- Given a candidate accepts an invite, When the seat is confirmed, Then the seat is tagged with the candidate’s partner ID, source, campaign, and policy version used for the decision. - Given a candidate declines or times out and another accepts, When attribution is applied, Then it reflects the accepting candidate’s partner only. - Given manual assignment by a captain, When accepted, Then attribution follows the candidate’s partner by default; any override attribution requires explicit selection and audit trail. - Given nightly reporting sync, When records are exported, Then partner credits in GiveCrew reports equal the count of accepted seats per partner for the event, excluding no-shows and declines.
Display Live Partner Mix and Explanations in Status Views and API
- Given an event is in fill or active state, When a captain views status, Then the UI shows current partner mix counts/percentages vs caps and targets with clear indicators for under/over balance. - Given any fairness rule causes a skip or selection, When viewing the candidate or seat, Then a reason tag is shown and an expanded explanation is available detailing rules triggered and inputs used. - Given an API client requests decision context, When calling the seat or candidate endpoint, Then a structured explanation object is returned including rule IDs, inputs (caps remaining, last invited), and outcome. - Given updates occur, When partner mix changes, Then UI refresh reflects changes within 5 seconds and API reflects within 1 second of persistence.
Overrides Require Audit Trail Without Violating Consent/Compliance
- Given a captain initiates an override affecting fairness, When confirming, Then the system requires a reason code, free-text note (min 10 chars), and captures user ID, timestamp, affected rule IDs, and pre/post allocation state. - Given an override would violate consent or compliance (e.g., no-contact flag, legal age), When attempted, Then the system blocks the action and surfaces the specific constraint; no data changes occur. - Given overrides exist, When generating reports or exports, Then overridden assignments are credited accordingly but are flagged and filterable by override status; audit log entries are immutable and verifiable.
Configurable Coalition Fairness Policies and Versioning
- Given coalition-level fairness policies are defined, When a new event is created, Then it inherits default caps, rotation strategy, and targets; authorized roles may adjust per-event settings within configured bounds. - Given a policy is updated mid-campaign, When changes are saved, Then the new version applies to future invitations only unless rebalancing is explicitly enabled; existing accepted seats are unaffected. - Given policy versioning, When fairness decisions are logged, Then each record references the policy version ID used; restoring a prior version re-applies those rules to new decisions.
Messaging Templates, Localization, and Compliance
"As a volunteer, I want clear, localized invites with simple opt-out so that I can respond quickly and stay in control of my participation."
Description

Provide customizable, multilingual invite and reminder templates for SMS and email with dynamic variables (event name, start time, seat hold timer, confirmation links). Support A/B variants, per-tenant branding, link tracking, and reply handling (YES/NO keywords) with automatic state updates. Enforce quiet hours, include required legal text and unsubscribe instructions, and propagate opt-outs globally. Offer fallback channels when primary delivery fails and surface deliverability metrics to the status view. Integrate with existing GiveCrew messaging providers and configuration systems.

Acceptance Criteria
Localized SMS Invite with Dynamic Variables
Given a phonebank event with populated variables (event_name, start_time, seat_hold_timer, confirmation_link) and a contact with preferred language = Spanish and SMS opt-in = true When SeatSaver Auto-Relay sends the T-10 invite Then the SMS is rendered from the Spanish template and all variables are correctly interpolated And the message is sent via the tenant’s configured SMS provider with template_id, language, and event_id logged And the confirmation_link is unique per contact and clickable with tracking parameters appended And the final SMS respects the tenant’s configured max segment count; otherwise the send is blocked and logged as validation_error
Email Reminder A/B Variant with Tenant Branding and Link Tracking
Given two active A/B variants for the reminder email (50/50 split) and tenant branding configured (logo, colors, footer) When reminder emails are sent for the event Then recipients are assigned to variants within ±2% of the target split And each email renders the correct tenant branding and required footer And all links include tracking parameters and preserve confirmation link integrity And opens and clicks are attributed to the correct variant and stored with message_id and contact_id
Keyword Reply Handling and Automatic State Update
Given an invite SMS instructing YES/NO and containing a confirmation link When a recipient replies YES (case-insensitive; accepts locale variants e.g., SI) Then their seat is confirmed, the ladder updates to filled for that contact, and a confirmation receipt is sent And duplicate YES replies are idempotent and do not create duplicate assignments When a recipient replies NO (case-insensitive; accepts locale variants e.g., NO) Then their seat hold is released immediately and the next ladder contact is invited And all reply events are logged with timestamp, normalized keyword, and resulting state change
Quiet Hours Enforcement and Deferred Send
Given tenant quiet hours of 21:00–08:00 evaluated in the recipient’s local time zone When an invite or reminder is scheduled during quiet hours Then the message is deferred to send at 08:00 local time and marked queued:quiet-hours And the audit log records defer_time, time_zone_source, and applied_rule_id And if the send window expires before 08:00, the message is canceled and the next eligible outreach step is executed per configuration
Legal Text, Unsubscribe Instructions, and Global Opt-Out
Given compliance text and unsubscribe instructions configured per tenant and locale When any SMS or email is sent Then the required legal text renders in the recipient’s language and unsubscribe mechanism is included (STOP for SMS; link for email) And the final SMS does not exceed the tenant-configured max segments; otherwise send is blocked with validation_error When a recipient unsubscribes (STOP or unsubscribe link) Then the contact is opted out across all GiveCrew campaigns and channels within 60 seconds And subsequent sends are suppressed and logged as suppressed:opt-out with reason and source
Fallback Channel on Primary Delivery Failure
Given SMS is the primary channel and email is available for the contact When an SMS send returns a final failure from the provider within 5 minutes Then an email fallback is sent within 2 minutes using the mapped template and identical dynamic variables And the status view shows the fallback action linking both message_ids and provider codes And only one active seat hold exists for the contact after fallback
Deliverability and Engagement Metrics in Status View
Given SeatSaver invites and reminders are in flight When the status view is opened Then it displays per-event and per-tenant aggregates: sent, delivered, failed, queued, bounced, open_rate, click_through_rate, reply_rate, opt_outs And metrics refresh at least every 60 seconds and show a last_updated timestamp And drilling into a metric reveals per-message logs including channel, template_id, provider_message_id, delivery_status, open/click timestamps

ReadyCheck Ping

Sends a light, two-tap attendance confirm at T-20/T-15 (“I’m in” or “Can’t make it”). Negative or no responses automatically trigger standby backfill, giving you a head start before T-10. Surfaces no-shows early, reduces scramble, and offers an instant reschedule link to keep volunteers engaged for the next shift.

Requirements

Two-Tap ReadyCheck Prompt
"As a shift volunteer, I want a quick two-tap prompt shortly before my shift so that I can confirm or decline with minimal friction."
Description

Implements a lightweight, two-action confirmation flow presented at T-20/T-15 prior to a scheduled shift, offering “I’m in” and “Can’t make it” options via push/in-app (with graceful handling for lock-screen actions). Captures response, timestamp, and delivery channel, updates the volunteer’s attendance status in real time, and debounces duplicate pings across devices. Handles edge cases such as overlapping shifts, late sign-ups, and previously confirmed volunteers, ensuring idempotent updates. Persists outcomes to the contact timeline and shift roster, enabling downstream automation (backfill triggers, alerts, and analytics).

Acceptance Criteria
T-20/T-15 ReadyCheck Scheduling
Given a volunteer is assigned to a shift with start time S and has notifications enabled When the system time reaches S minus 20 minutes Then a ReadyCheck prompt is sent via push within 60 seconds And if the S minus 20 window was missed, a ReadyCheck prompt is sent at S minus 15 minutes And no more than one ReadyCheck is sent per shift per volunteer between S minus 20 and S minus 10 minutes
Lock-Screen Quick Action
Given a ReadyCheck push with actionable buttons is delivered to a compatible device's lock screen When the volunteer taps 'I'm in' or 'Can't make it' from the lock screen Then the system records the response with server-received timestamp, delivery_channel='lock_screen_push', and device_id And the response is acknowledged to the device within 3 seconds
Late Sign-Up Within ReadyCheck Window
Given a volunteer is assigned to a shift after S minus 20 minutes When the assignment is saved Then if the current time is at or before S minus 15 minutes, a ReadyCheck is sent within 60 seconds of assignment And if the current time is after S minus 15 minutes but at or before S minus 10 minutes, a ReadyCheck is sent immediately And if the current time is after S minus 10 minutes, no ReadyCheck is sent
Real-Time Status Update and Persistence
Given a valid ReadyCheck response is received for a shift When the response is processed Then the volunteer's attendance_status for that shift updates to 'ready' for 'I'm in' or 'declined' for 'Can't make it' within 5 seconds And the shift roster reflects the new status within 5 seconds And a timeline event 'ReadyCheck Response' is persisted with fields shift_id, volunteer_id, response, delivery_channel, received_at, device_id
Duplicate Ping Debounce Across Devices
Given the volunteer is signed in on multiple devices that receive the same ReadyCheck When the volunteer responds on any one device Then only the first valid response is persisted And subsequent responses within 10 minutes are logged as outcome='ignored_duplicate' without changing attendance_status or creating additional timeline events And outstanding prompts on other devices are dismissed or marked as already responded
Overlapping Shifts and Prior Confirmation
Given a volunteer has two shifts whose times overlap When ReadyCheck prompts are sent Then each prompt includes that shift's title and start time, and responding to one updates only that shift's attendance_status and timeline And if a shift was already confirmed before the ReadyCheck window, no ReadyCheck is sent for that shift And if a ReadyCheck for a shift has already been answered, no additional ReadyCheck is sent for that shift
Negative/No Response Backfill and Reschedule
Given a volunteer taps 'Can't make it' before S minus 10 minutes When the response is processed Then a backfill_trigger event is emitted within 5 seconds with payload containing shift_id, volunteer_id, reason='cant_make_it', responded_at And the volunteer is presented an instant reschedule link to available upcoming shifts Given no ReadyCheck response is received by S minus 10 minutes When the deadline is reached Then a backfill_trigger event is emitted within 5 seconds with payload containing shift_id, volunteer_id, reason='no_response', deadline_at
Configurable Ping Windows & Rules
"As an organization admin, I want to configure when and to whom ReadyCheck pings are sent so that the timing and rules fit our programs and timezones."
Description

Provides organization-, program-, and shift-level configuration of ReadyCheck timing and logic, including default ping windows (e.g., T-20/T-15), per-shift overrides, blackout hours, minimum lead times, and cutoffs to avoid sending after a shift starts. Supports timezone-aware scheduling with DST handling and prevents duplicate pings when shifts are updated. Includes eligibility rules (e.g., only unconfirmed volunteers, exclude already checked-in, respect channel opt-outs) and environment toggles for pilots/rollouts. Exposes admin UI with safe defaults and audit of changes.

Acceptance Criteria
Precedence of Org/Program/Shift Ping Settings
Given an org default ping windows of T-20 and T-15 And a program override of T-30 and T-20 And a shift-level override specifying only T-10 When a shift under that program starts at 2025-08-15 18:00 in its local timezone Then exactly one ping is scheduled per eligible volunteer at 17:50 local And no pings are scheduled at 17:40 or 17:30 And for a shift in the same program without a shift override, pings are scheduled at 17:30 and 17:40 local And for a shift in a different program without overrides, pings are scheduled at 17:40 and 17:45 local
Blackout, Minimum Lead Time, and After-Start Cutoffs
Given blackout hours 21:00–08:00 local and a minimum lead time of 30 minutes And a shift start at 08:15 local with windows T-30 and T-15 When scheduling pings Then the T-30 ping at 07:45 is suppressed due to blackout And the T-15 ping at 08:00 is sent And no ping is sent at or after 08:15 And if a shift is created at 08:00 for 08:20 start, no ping is sent because minimum lead time is not met And if all windows are suppressed by blackout and cutoffs, no fallback pings are sent
Timezone and DST-Accurate Scheduling
Given a shift located in America/New_York starting 2025-03-09 09:00 with windows T-20 and T-15 When scheduling pings Then pings are scheduled at 08:40 and 08:45 America/New_York time And all volunteer recipients in other timezones still receive at those America/New_York instants And for a shift in Europe/London on 2025-10-26 09:00 (DST end), pings are scheduled at 08:40 and 08:45 Europe/London time without duplication or drift
No Duplicate Pings on Shift or Roster Updates
Given a queued T-20 ping for shift S at 18:00 When the shift start time changes to 19:00 Then the original T-20/T-15 jobs are canceled And new T-20/T-15 jobs are queued for 18:40 and 18:45 And only one message per volunteer per window is delivered And when a volunteer is removed from the roster before any ping is sent, their queued pings are canceled And when a volunteer is added after T-20 but before T-15, only the T-15 ping is scheduled for them if eligibility is met
Eligibility Filtering and Response Suppression
Given a roster with volunteers V1 (unconfirmed), V2 (confirmed), V3 (checked in), V4 (unconfirmed with SMS opt-out) When pings are sent Then only V1 receives the ping via available channels And V2 and V3 receive no pings And V4 receives no SMS; if email or push are enabled and not opted out, they are used; otherwise no ping is sent And if V1 responds “I’m in” after the first window, the second window ping is automatically suppressed And if V1 responds “Can’t make it,” no further pings for that shift are sent to V1
Pilot and Rollout Environment Toggles
Given an org with ReadyCheck set to Pilot=Off and Production=Off When a ping window is reached Then no live messages are sent and an audit log entry is created with mode=disabled And when Pilot=On and Production=Off for Program P, pings are sent only for shifts under Program P and marked mode=pilot And when Production=On, pings are sent for all in-scope programs and shifts regardless of Pilot flag And toggling any mode takes effect for windows not yet executed and does not re-send already-sent pings
Admin UI Defaults, Validation, and Audit Trail
Given a new organization Then default settings are pre-populated: windows T-20 and T-15; blackout 21:00–08:00; minimum lead time 30 minutes; after-start cutoff enabled; timezone inherited from org locale And the admin can change settings at org, program, and shift scope with clear indication of inheritance And invalid inputs (negative lead time, overlapping blackout intervals, malformed timezone) prevent save with inline error messages And every change writes an immutable audit record capturing actor, UTC timestamp, scope, fields before/after, and optional reason And the audit log is viewable in UI and exportable as CSV And changes apply only to pings scheduled after the change; previously queued jobs are not modified
Auto-Backfill Trigger & Standby Orchestration
"As a coordinator, I want the system to auto-trigger standby backfill on negative or no responses so that open slots are filled before the shift begins."
Description

Automatically initiates backfill when a volunteer responds “Can’t make it” or fails to confirm by a threshold (e.g., T-12/T-10). Pulls from the standby/waitlist pool using ranked criteria (skills, proximity, past reliability, availability), sends time-bound invitations, and auto-assigns the first acceptance until shift capacity is met. Includes throttle and stop conditions, conflict checks, and escalation to coordinators if no substitutes accept. Writes all actions to the roster and timelines, and prevents overfilling through atomic updates. Integrates with existing assignment and messaging services.

Acceptance Criteria
Trigger Backfill on “Can’t make it” Response
Given a shift with ReadyCheck Ping enabled and a backfill threshold of T-10 And the scheduled volunteer responds “Can’t make it” at any time from T-20 up to T-10 When the response is received by the system Then a vacancy is opened for the shift within 2 seconds And the backfill workflow is enqueued within 5 seconds And the first invitation wave to standby candidates is dispatched within 10 seconds of the response And the volunteer is marked canceled on the roster with reason “declined”
Trigger Backfill on No-Response at T-10 Threshold
Given a shift with ReadyCheck Ping enabled and a backfill threshold of T-10 And the scheduled volunteer has not responded “I’m in” by T-10 When the system time reaches T-10 Then a vacancy is opened and the backfill workflow is enqueued within 5 seconds And the first invitation wave is dispatched within 10 seconds of T-10 And no duplicate backfill is triggered if a “Can’t make it” arrives after T-10 And if the volunteer later confirms while capacity is still available, they may be re-assigned and pending invites for that slot are canceled; otherwise their confirmation is declined with “slot filled”
Ranked Standby Selection with Throttled Invitation Waves
Given a standby/waitlist pool with attributes skills, proximity, past reliability, and availability When generating a backfill wave Then candidates are ranked by the configured scoring model using those attributes And the wave includes up to the throttle limit (default 3) highest-ranked eligible candidates And candidates previously invited for the same vacancy, already assigned, or in conflict are excluded And each candidate receives a unique, single-use invite link
Time-Bound Invitations and Wave Progression
Given a backfill invitation wave has been sent When an invite is issued to a candidate Then the invite expires at the earlier of 5 minutes after send or T-5 And if no candidates accept before all current invites expire, the next wave is dispatched within 10 seconds, repeating until capacity is met or the stop condition is reached And invites clearly state time remaining and provide one-tap accept/decline
Auto-Assign First Acceptance with Capacity Stop and Atomic Updates
Given one or more candidates respond to invites When the first acceptance is received for an open slot Then the candidate is auto-assigned to the shift within 3 seconds And all other pending invites for that specific slot are immediately canceled And capacity is not exceeded under concurrent accepts due to atomic assignment updates; late accepts receive “slot filled” within 3 seconds And the backfill process stops for that slot when shift capacity is met or the stop condition time is reached (whichever first)
Pre-Assignment Conflict and Eligibility Checks
Given a candidate is about to be auto-assigned via backfill When eligibility checks are performed Then the system validates required skills/role match, no overlapping assignments during the shift time, the candidate is not already on the roster for this shift, and declared availability includes the shift window And if any check fails, the assignment is blocked, the candidate is notified, the reason is recorded, and the next eligible candidate is considered without delaying the wave schedule
Escalation to Coordinators and End-to-End Audit Logging
Given a backfill process is active for a vacancy When no substitutes accept by the earlier of two full waves completing or T-8 Then an escalation notification is sent to assigned coordinators via the configured channels (push/SMS/email) with a summary (vacancies, invites sent, responses, time remaining) And the system continues backfill attempts until the hard stop (shift start) unless a coordinator stops it And all backfill-related actions (triggers, invites, responses, assignments, cancellations, escalations, errors) are written to the roster and timeline with timestamps and correlation IDs And all communications are sent via the existing messaging service and all assignments via the existing assignment service; failures are retried up to 3 times with exponential backoff and are visible in the timeline
Instant Reschedule Path for Declines
"As a volunteer who can’t attend, I want an instant reschedule option so that I can quickly commit to a future shift and stay engaged."
Description

Offers a one-tap reschedule flow when a volunteer selects “Can’t make it,” presenting smart suggestions for comparable upcoming shifts (same location/role/time window) and allowing immediate booking without re-entering details. Captures cancellation reason and optional note, updates commitment metrics, and sends confirmations. Ensures the decline closes the current slot and does not block backfill. Respects volunteer preferences and prevents double-booking by checking conflicts. Exposes deep links in SMS/email as fallback for device notifications.

Acceptance Criteria
Reschedule Prompt on Decline from ReadyCheck Ping
Given a volunteer taps "Can't make it" on a ReadyCheck Ping for an assigned shift When the decline is submitted Then the reschedule screen opens within 2 seconds with the shift’s location, role, and time prefilled And the original shift assignment is set to Open immediately And the standby backfill workflow is triggered within 60 seconds And no login or data re-entry is required
Smart Suggestions for Comparable Shifts
Given a decline event for Shift S (location L, role R, start time Ts) When suggestions are generated Then the first group includes shifts with the same L and R, starting in the same time window (morning/afternoon/evening) within the next 7 days And at least 3 suggestions are shown if available; if fewer than 3 exist, show all matches and then next-priority matches (same R within 10 miles and same time window) until up to 5 total And suggestions respect the volunteer’s stated preferences (availability, location radius, role opt-ins) and blackout dates And suggestions are sorted by soonest start time and display a "Similar because …" explanation And no duplicate, full, or waitlisted shifts are shown
One-Tap Booking Without Re-Entering Details
Given the reschedule screen is displayed When the volunteer taps a suggested shift Then the new shift is booked immediately using existing profile data, consents, and preferences without requiring any additional fields And a success confirmation screen shows within 2 seconds with the new shift details And a confirmation SMS/email is sent within 60 seconds and the volunteer’s calendar integrations are updated And suggestions that would require missing mandatory data or expired waivers are suppressed from the list
Conflict and Double-Booking Safeguards
Given the volunteer has existing commitments When evaluating a suggested shift Then any shift overlapping or starting within 30 minutes of another commitment is excluded And if the selected shift becomes unavailable due to a race condition, booking is blocked and the user sees "Slot just filled" with refreshed suggestions within 2 seconds And if the volunteer opens a deep link to a conflicting shift, the system prevents booking and surfaces the next-best alternatives
Capture Cancellation Reason and Optional Note
Given the volunteer selects "Can't make it" When the decline is submitted Then a reason must be selected from a configurable list and an optional note up to 280 characters may be provided And the reason and note are stored on the original shift record and associated with the volunteer profile And commitment metrics (cancellations count, lead time to cancel, show-up rate) are updated within 5 minutes
Backfill Non-Blocking Guarantee
Given a decline is initiated When the reschedule flow is presented Then the original slot remains open and eligible for backfill regardless of whether the volunteer completes rescheduling And if the volunteer successfully reschedules, the original slot remains canceled and is not re-assigned to them And standby/backfill notifications are not delayed by the presence of the reschedule screen
Confirmation and Fallback Deep Links
Given a volunteer completes a reschedule or only declines When notifications are sent Then the volunteer receives an in-app confirmation and an SMS and/or email with a deep link to the new shift or reschedule options within 60 seconds And deep links open the app (or mobile web) directly to the target screen with context preloaded and expire after 24 hours And links work on iOS, Android, and mobile web and are protected by a time-bound token; on failure, a generic login path is provided
Early No‑Show Surfacing & Coordinator Alerts
"As a coordinator, I want early insight into who hasn’t confirmed so that I can act before the shift starts and avoid last‑minute scramble."
Description

Provides real-time visibility of unconfirmed or at-risk volunteers on the Shift Roster and Impact Board, including counts and names grouped by status (Confirmed, Declined, Unreached). Sends configurable alerts to shift leads via mobile push, SMS, or email when thresholds are met (e.g., more than N unconfirmed at T-10). Includes quick actions to call/text volunteers, trigger manual backfill, or pause alerts. Generates post-shift summaries with response rates and show-up deltas to inform staffing and playbooks.

Acceptance Criteria
Shift Roster Real-Time Status Grouping
Given a shift roster with assigned volunteers and active ReadyCheck pings When a volunteer taps "I'm in" Then their status changes to Confirmed within 5 seconds and they appear under the Confirmed group by name And the Confirmed group count increments by 1 When a volunteer taps "Can't make it" Then their status changes to Declined within 5 seconds and they appear under the Declined group by name And the Declined group count increments by 1 When a volunteer does not respond by the ping cutoff Then their status remains Unreached and they appear under the Unreached group by name And each group header count equals the number of names in that group And the total assigned count equals Confirmed + Declined + Unreached
Impact Board Real-Time At-Risk Display
Given the Impact Board is open in live mode for an active shift When volunteer statuses change (Confirmed, Declined, Unreached) Then the board updates counts by status within 5 seconds without page refresh And Unreached and Declined counts are visually flagged as at-risk per design tokens And tapping the shift tile reveals names grouped by status without exposing phone numbers in public-safe mode And the board shows last-updated timestamp no older than 10 seconds
T-10 Unconfirmed Threshold Alert (Multi-Channel)
Given a shift with an alert threshold of N unconfirmed and lead channels set to Push, SMS, and Email And the system clock is synchronized within ±2 seconds When the relative time reaches T-10 and Unreached count exceeds N Then the shift lead receives a push notification within 10 seconds containing shift name/time, counts by status, and names of Unreached And if push is not delivered within 20 seconds, an SMS is sent automatically with the same core content (truncated if needed) And an email is delivered within 60 seconds with full details and quick action links And the alert includes deep links to Call/Text/Backfill and an option to Pause alerts for this shift
Alert Configuration and Quiet Hours
Given a coordinator opens Shift Settings > Alerts When they set the unconfirmed threshold to N (1–50) and choose channels per lead Then the configuration saves successfully and is applied to the shift within 5 seconds When quiet hours are set (e.g., 21:00–08:00 local) Then alerts scheduled inside quiet hours are suppressed for Email/SMS and queued for Push only, unless Override is enabled And a test-alert action sends a non-production message to all selected channels and records deliverability results
Alert Deduplication and Escalation Fallback
Given an alert has been sent for exceeding the unconfirmed threshold When additional Unreached changes occur within 5 minutes that do not reduce the count below threshold Then duplicate alerts are suppressed for that window and a single consolidated update is sent at window end And if no channel confirms delivery (Push receipt or SMS delivered) within 30 seconds, fallback sends to the next available channel And all alert sends and suppressions are logged with timestamp, channel, and delivery status
Quick Actions from Alert and Roster
Given a lead opens an alert or the Shift Roster When they tap Call next to a volunteer Then the device native dialer opens with the volunteer’s number populated When they tap Text Then the device SMS composer opens with a prefilled, editable template including shift name/time When they tap Manual Backfill Then top K standby volunteers are notified within 10 seconds via their preferred channel with an accept/decline prompt And accepted backfills appear on the roster within 5 seconds of acceptance When they tap Pause Alerts and choose 10 minutes Then further threshold alerts are suppressed for 10 minutes for that shift and a resume time is displayed
Post-Shift Summary and Metrics
Given a shift has ended When the post-shift job runs within 15 minutes after end time Then a summary is generated with: total assigned, Confirmed/Declined/Unreached counts and rates, show-up count, and show-up delta vs Confirmed And the summary is delivered to designated leads via email and is viewable in the app under Shift > Reports And exported CSV metrics match the in-app counts and the event log within ±1 volunteer And failed summary deliveries are retried up to 3 times with exponential backoff and logged
Multi-Channel Delivery & Compliance Guardrails
"As an admin, I want ReadyCheck messages sent over the best available channel while honoring compliance and preferences so that we reach volunteers reliably and safely."
Description

Delivers ReadyCheck via prioritized channels (push > SMS > email) with automatic fallback, per-user preferences, and localized message templates. Enforces consent/opt-in, opt-out handling, sender ID/shortcode constraints, quiet hours, and regional regulations. Implements rate limiting, retry policies, and delivery/response logging for observability. Provides admin controls for template editing and A/B tests, and exposes delivery metrics (sent, delivered, bounced, responded) for optimization. Integrates with existing comms providers and supports failover.

Acceptance Criteria
Prioritized Multi-Channel Delivery with Fallback
- Given a volunteer with push notifications enabled and a valid device token, when ReadyCheck is scheduled at T-20 or T-15 for their upcoming shift, then the system sends a push notification and does not send SMS or email unless fallback is triggered. - Given push delivery confirmation is not received within 10 seconds or a push send error occurs, when fallback logic runs, then an SMS is sent via the user's permitted SMS channel. - Given SMS delivery confirmation is not received within 60 seconds or the SMS provider is unavailable, when fallback logic runs, then an email is sent to the volunteer's verified email address. - Given the volunteer has opted out of a channel or disabled it in preferences, when delivery is attempted, then that channel is skipped at all stages of the fallback sequence. - Given multiple send attempts occur for the same ReadyCheck, when messages are delivered, then the volunteer receives at most one channel notification and duplicates are suppressed by idempotency. - Given the ReadyCheck window opens, when delivery is initiated, then the first send attempt occurs within 2 seconds of the scheduled T-20/T-15 mark in the volunteer's timezone.
Consent and Opt-In/Opt-Out Enforcement
- Given a volunteer without explicit SMS consent, when a ReadyCheck is initiated, then no SMS is sent and a "blocked_by_consent" event is logged with timestamp and channel=SMS. - Given a volunteer replies STOP to a ReadyCheck SMS, when the message is processed, then the system immediately suppresses future SMS to that number, logs the opt-out with source=keyword, and sends a single confirmation SMS per carrier rules. - Given a volunteer clicks an email unsubscribe link, when the event is received, then the system suppresses future emails to that address and logs the opt-out with source=link. - Given a volunteer sends START to the previously opted-out SMS number, when the keyword is processed, then SMS consent is restored and a confirmation SMS is sent. - Given an admin attempts to send a ReadyCheck to a channel the volunteer has opted out of, when the send is queued, then the message is not sent and the UI/API indicates the suppression reason. - Given an outbound SMS to US recipients, when the message is constructed, then it includes the organization name and "Reply STOP to opt out".
Quiet Hours and Regional Compliance Guardrails
- Given an org quiet hours policy of 21:00–08:00 local time, when a volunteer's T-20/T-15 falls within quiet hours, then the ReadyCheck is queued to send at 08:00 if it is still at least 15 minutes before shift start; otherwise it is suppressed and a "suppressed_by_quiet_hours" event is logged. - Given the volunteer's timezone is known, when quiet hours are evaluated, then the policy is applied using the volunteer's local time. - Given a volunteer's country requires short code or toll-free verified sender for SMS, when an SMS is sent, then the message uses a compliant sender ID for that country. - Given no compliant sender is configured for the volunteer's country, when an SMS send is attempted, then the send is blocked, the event is logged as "blocked_by_sender_policy", and alternate channels continue per fallback. - Given alphanumeric sender IDs are prohibited in the destination country, when the SMS is constructed, then a numeric sender is used.
Rate Limiting, Retry, and Provider Failover
- Given a per-provider rate limit of 10 requests per second is configured, when 1,000 SMS messages are sent, then the send rate per provider does not exceed 11 requests per second and no provider returns 429 due to client-side overrun. - Given a transient 5xx error is returned by the provider, when the send is retried, then the system retries up to 3 times with exponential backoff (e.g., 1s, 2s, 4s) before marking as failed. - Given the primary SMS provider health check fails or the rolling 1-minute error rate exceeds 20%, when new SMS sends are queued, then they are routed to the secondary provider until the primary recovers. - Given a retry or failover occurs, when messages are sent, then idempotency keys ensure the volunteer receives at most one SMS for that ReadyCheck. - Given the ReadyCheck send time is reached, when queueing and rate limiting are applied, then 95% of first-attempt send requests are issued within 2 seconds of the scheduled time.
Delivery and Response Logging for Observability
- Given a ReadyCheck is sent, when events occur, then the system logs state transitions: queued, attempted, sent, delivered, bounced/failed, responded_yes, responded_no, with timestamps and correlation ID. - Given providers post delivery receipts or inbound replies to webhooks, when the webhook is received, then the corresponding log entry is updated within 60 seconds and deduplicated by provider message ID. - Given an admin views the Communication Log, when they filter by shift, channel, status, or volunteer, then matching records are returned within 2 seconds and can be exported to CSV. - Given a volunteer responds "I'm in" or "Can't make it", when the response is processed, then the associated volunteer shift attendance status is updated and the response event is linked to the original send.
Admin Template Editing and Localization
- Given an admin with Template Editor permission, when they open ReadyCheck templates, then they can create, edit, and save templates for push, SMS, and email with placeholders for {first_name}, {shift_title}, {shift_start_time}, and {reschedule_link}. - Given locales en-US and es-ES are configured, when a volunteer's preferred language is es-ES, then the Spanish template is selected; if missing, the org default locale template is used. - Given an SMS template exceeds 160 GSM-7 characters, when previewed, then the UI shows the segment count and warns if it exceeds 2 segments. - Given a template is published as a new version, when a ReadyCheck is sent, then the published version is used for new sends and in-flight messages continue with their original version. - Given regional SMS compliance rules, when templates are saved, then validation ensures required elements (org name, opt-out text) are present and blocked content is rejected with an error.
Metrics and A/B Testing for Optimization
- Given ReadyCheck sends and responses occur, when metrics are queried, then counts for sent, delivered, bounced, responded_yes, responded_no, and response_rate are available per shift, per channel, and per org. - Given new events are processed, when the dashboard refreshes, then metrics reflect events within 2 minutes of occurrence. - Given an A/B test is configured with two SMS templates at a 50/50 split, when 1,000 volunteers are notified, then assignment per variant is 50% ±2% and each volunteer receives exactly one variant. - Given an A/B test runs, when metrics are viewed, then per-variant response rates and counts are displayed and the higher response rate variant is flagged as current leader. - Given an A/B test is stopped and a winner selected, when subsequent ReadyChecks are sent, then the winning variant is used for 100% of traffic.
Attendance State Model & Audit Trail
"As a data steward, I want a consistent attendance model and audit log so that we can reconcile records and report ReadyCheck impact with confidence."
Description

Defines a clear attendance lifecycle for ReadyCheck (Scheduled → ReadyPinged → Confirmed → Declined → No Response → Backfilled/Cancelled → Checked In/No-Show) with deterministic transitions and time-based rules. Records an immutable audit trail of pings, responses, assignments, and backfill events with actor, timestamp, and channel. Exposes states to the API, exports, and analytics, enabling accurate reporting on show-up rates and the impact of ReadyCheck. Ensures data consistency across mobile and web through optimistic concurrency and conflict resolution.

Acceptance Criteria
Auto-Transition: Scheduled → ReadyPinged → Confirmed/Declined/No Response
- Given an assignment in Scheduled and the current time reaches the first ReadyCheck window (T-20) or next window (T-15), When the system sends a ReadyCheck ping via the volunteer’s preferred channel, Then the state changes to ReadyPinged and an audit event ReadyCheckSent is appended with actor=system, channel, and timestamp. - Given an assignment in ReadyPinged, When the volunteer taps “I’m in” before T-10, Then the state changes to Confirmed and an audit event ReadyCheckResponded is appended with actor=volunteer, response=confirm, channel, and timestamp. - Given an assignment in ReadyPinged, When the volunteer taps “Can’t make it” before T-10, Then the state changes to Declined and an audit event ReadyCheckResponded is appended with actor=volunteer, response=decline, channel, and timestamp. - Given an assignment in ReadyPinged, When no response is received by T-10, Then the state changes to NoResponse and an audit event NoResponseElapsed is appended with actor=system and timestamp. - Given an assignment that is Confirmed or Declined, When the next ping window occurs (e.g., T-15), Then no additional ReadyCheck ping is sent and no duplicate ReadyCheckSent audit is created. - Given an assignment already ReadyPinged at T-20, When T-15 occurs, Then no duplicate ReadyCheck ping is sent unless explicitly triggered by a user action (which is separately audited).
Backfill Initiation on Negative/No Response
- Given an assignment transitions to Declined or NoResponse, When the transition is committed, Then a backfill job is queued within 5 seconds and an audit event BackfillQueued is appended with actor=system and timestamp. - Given backfill outreach results in a replacement volunteer accepting, When the replacement assignment is created, Then the original assignment transitions to Backfilled, the replacement assignment is created with its own state (Scheduled or Confirmed as applicable), and an audit event BackfilledWithAssignment is appended linking original and replacement assignment IDs. - Given a coordinator chooses not to backfill the slot, When they cancel the slot, Then the original assignment transitions to Cancelled and an audit event SlotCancelled is appended with actor=user and timestamp. - Given an original assignment is Backfilled or Cancelled, When a late ReadyCheck response arrives from the original volunteer, Then the state does not change, the response is recorded as an audit event LateResponse with actor=volunteer and channel, and no backfill is revoked automatically.
Immutable Audit Trail with Actor, Timestamp, Channel
- Given any state transition or ReadyCheck-related action (ping sent, response received, backfill queued, assignment created, check-in, no-show), When it occurs, Then an audit entry is appended containing: assignmentId, previousState, newState, eventType, actorType (system|user|volunteer), actorId (nullable), channel (sms|push|web|api|null), timestamp in UTC with source timezone offset, and metadata digest. - Given an API client attempts to edit or delete an existing audit entry, When the request is processed, Then the system rejects it with 405 Method Not Allowed and no changes are made to stored audit entries. - Given audit entries are written for an assignment, When they are persisted, Then they are immutable and ordered by a monotonically increasing sequenceNumber per assignment, guaranteeing replay in exact order. - Given an audit export is requested for a date range, When the export completes, Then 100% of audit entries in range are included in chronological order with sequenceNumber, and the total count matches the count returned by the API preflight within ±0 discrepancy.
State Exposure in API, Exports, and Analytics
- Given a GET /assignments/{id} request, When the assignment exists, Then the response includes currentState (enum: Scheduled|ReadyPinged|Confirmed|Declined|NoResponse|Backfilled|Cancelled|CheckedIn|NoShow), lastTransitionAt (ISO-8601 UTC), and allowedNextStates per the deterministic state graph. - Given a list/export of assignments is generated, When the file is produced, Then each row includes currentState, lastTransitionAt, readyCheckPingsSentCount, readyCheckResponsesCount, and finalAttendanceState (CheckedIn|NoShow|null) for reporting. - Given the Analytics module runs the ReadyCheck Impact report, When data is aggregated, Then it can compute: confirmRate = Confirmed/ReadyPinged, earlySurfaceRate = (Declined+NoResponse before T-10)/ReadyPinged, showUpRate = CheckedIn/Confirmed, each filterable by channel and time window. - Given a client attempts to set or query an invalid state, When the API processes the request, Then it returns 400 Bad Request with validation errors and does not create or persist invalid states.
Optimistic Concurrency and Conflict Resolution
- Given an assignment resource is fetched, When the client updates state, Then the request must include the latest version/etag; otherwise the API returns 409 Conflict with the current version and no transition is applied. - Given two conflicting transitions arrive close in time (e.g., Confirmed via mobile and Declined via web), When processed, Then the server applies the first valid transition by server-received timestamp and rejects the second with 409 Conflict; both attempts are recorded in audit (one TransitionCommitted, one RejectedTransition with reason=conflict). - Given a client retries after receiving 409 Conflict with the latest version, When the same transition is resubmitted idempotently, Then no duplicate transition or duplicate audit entry is created. - Given a transition is not allowed from the current state by the deterministic graph, When requested, Then the API returns 422 Unprocessable Entity and appends a RejectedTransition audit with reason=invalidTransition; UI and API reflect the unchanged state within 2 seconds.
Finalization: Checked In vs No-Show with Grace Period
- Given an assignment in Confirmed, When the volunteer is checked in by a coordinator or self-check-in occurs between T-10 and shift start + 10 minutes (grace), Then the state transitions to CheckedIn and an audit event CheckIn is appended with actor and channel. - Given an assignment in Confirmed with no check-in by shift start + 10 minutes, When the grace period elapses, Then the state transitions to NoShow and an audit event NoShowElapsed is appended with actor=system and timestamp. - Given an assignment in Declined, Backfilled, or Cancelled, When the shift starts and grace elapses, Then it does not transition to NoShow and no NoShow audit is created for that assignment. - Given an assignment in NoResponse without a replacement, When the shift start + 10 minutes is reached, Then the state transitions to NoShow and an audit event NoShowElapsed is appended; if a replacement exists (Backfilled), the original remains Backfilled and does not generate NoShow.

SeatPass Handoff

Generates a secure, expiring link that drops an alternate directly into the correct dialer seat or breakout with pre-scoped permissions. Display name, script pack, and region are auto-applied, and Attribution Shield/Consent Ledger keep credit and compliance intact. Alternates become productive in seconds—no manual invites or setup.

Requirements

Secure Expiring SeatPass Link
"As an organizer, I want to generate a secure, expiring SeatPass link that routes an alternate into the correct seat so that they can join safely and immediately without manual setup."
Description

Generate a signed, time-bound, single-use link that deep-links an alternate directly into the intended dialer seat or breakout. The link embeds seat/breakout identifiers, role scope, and expiration (configurable TTL) and is validated server-side before session creation. Supports mobile app and responsive web dialer routing with app-links/universal-links and graceful web fallback. Enforces one-time redemption, optional PIN, and domain/channel allowlists to mitigate abuse. On validation, creates a short-lived session handshake without requiring full account setup. Integrates with GiveCrew identity, seat inventory, and routing services to ensure the user lands in the exact context with minimal friction while preserving security.

Acceptance Criteria
In-TTL SeatPass redemption lands user in exact seat/breakout
Given a valid SeatPass link signed by the server with seat_id/breakout_id, scope, and a TTL, and the seat is available And the current time is within the TTL window When the alternate opens the link on a supported device Then the server validates signature, TTL, scope, allowlist, and seat availability And creates a short-lived session handshake (per server-configured TTL) without provisioning a full account And routes the user directly into the specified seat or breakout And applies the scoped context (role permissions, display name, script pack, region) And marks the SeatPass as redeemed
Expired or tampered SeatPass is rejected without session
Given a SeatPass link whose TTL has elapsed or whose signature/payload has been altered When the link is opened Then the server denies redemption and returns an error indicating expiration or tampering And no session is created and no seat assignment occurs And the SeatPass remains unredeemed (tampered) or is marked expired (TTL elapsed) And the event is audit-logged with reason
One-time redemption enforced across devices and sessions
Given a SeatPass that has not yet been redeemed When it is successfully redeemed the first time Then the SeatPass is marked redeemed with a timestamp and redeemer fingerprint When any subsequent open occurs from any device, browser, or channel Then redemption is denied with an "Already redeemed" response And no additional session is created and no seat changes occur
PIN-protected SeatPass requires correct PIN to redeem
Given a SeatPass configured with an optional PIN code When the alternate opens the link and enters the correct PIN Then server proceeds with validation and session creation When an incorrect PIN is entered Then redemption is denied and the SeatPass is not marked as redeemed And an error is shown and the attempt is audit-logged
Domain and channel allowlist restricts redemption to approved origins
Given domain and channel allowlists configured for SeatPass redemption (e.g., approved app package IDs, universal link domains, and referrers) When the link is opened from a non-allowed domain, app, or channel Then redemption is blocked before session creation And the response indicates the origin is not allowed And the attempt is audit-logged without consuming the SeatPass
Cross-platform deep link routing with graceful web fallback
Given a SeatPass link with app-links/universal-links configured and a responsive web fallback When opened on iOS or Android with the GiveCrew app installed Then the native app is launched and the user is routed into the exact seat/breakout with context preserved When opened on iOS or Android without the app installed Then the responsive web dialer opens with the same context applied When opened on desktop browsers Then the responsive web dialer opens with the same context applied And in all cases the deep-link parameters (seat_id/breakout_id, scope) are preserved end-to-end
Seat inventory and routing enforce exact context and prevent race conditions
Given the same SeatPass link is opened near-simultaneously by multiple users or from multiple devices When server-side validation and seat inventory checks run Then only the first valid request creates a session and reserves the intended seat/breakout And all subsequent requests receive a "Seat unavailable or already redeemed" response And no conflicting sessions are created and seat inventory reflects the single reservation
Context Auto-Apply & Seat Provisioning
"As an alternate volunteer, I want my name, scripts, and region to auto-apply when I join so that I can start calling immediately with the right materials."
Description

Automatically apply the correct operational context when a SeatPass is redeemed, including display name, script pack, target region, language, and campaign metadata. Atomically claim and lock the designated seat or breakout, update presence, and load the current scripts and compliance prompts. For alternates without accounts, create an ephemeral participant profile scoped to the session. Ensure idempotent joins and resume-on-refresh. Reduces setup errors, accelerates time-to-first-call, and standardizes session configuration across mobile and web.

Acceptance Criteria
SeatPass Redemption Applies Full Context
Given a valid, unexpired SeatPass token referencing campaign C with script pack SPvN, region R, language L, and display name D When an alternate opens the SeatPass link and the client validates the token Then the session display name is set to D within 2 seconds And the active script pack version equals SPvN within 3 seconds And the target region is set to R and language to L before the dialer is enabled And campaign metadata M is injected into session state and visible in the dialer header And the compliance prompts for campaign C are loaded before the "Start Calling" control is enabled
Atomic Seat/Breakout Claim With Concurrency Control
Given seat S or breakout B is available And two or more users attempt to redeem the same SeatPass within 1 second When redemption requests reach the server Then exactly one redemption succeeds and S/B transitions to Claimed-Locked atomically And losing attempts receive HTTP 409 with error code SEAT_ALREADY_CLAIMED and no session context is applied And the winning session receives a seat lock TTL of at least 5 minutes, auto-renewed while connected And an audit log entry records winner user-id or ephemeral-id, seat/breakout id, and timestamp
Ephemeral Profile Creation For Non-Account Alternates
Given an alternate without a GiveCrew account redeems a SeatPass When the token is validated Then an ephemeral profile is created scoped to campaign C and seat/breakout S/B only And the profile cannot access organization settings or other campaigns (403 on restricted endpoints) And the profile expires and is deleted within 10 minutes after session end or token expiry, whichever is sooner And stored PII is limited to display name and session contact and is purged on expiry
Idempotent Join And Resume-On-Refresh
Given a user has redeemed a SeatPass and is connected to seat/breakout S/B When the user refreshes the page or reopens the same SeatPass within 15 minutes Then the same session is resumed without creating a duplicate presence record And the seat/breakout lock persists and dialer state (script pack version, region, language) is restored And if a second tab/window is opened, the original tab remains active and the new tab shows "Already joined" with an option to switch And no additional attribution or consent entries are created on resume
Presence Update And Compliance Gate Before Dialer Enable
Given a successful SeatPass redemption When the client connects to the real-time service Then the user's presence changes to On-Seat within 2 seconds and is visible to supervisors And compliance prompts/acknowledgments for campaign C are displayed and must be accepted before calls can start And acceptance is recorded in the Consent Ledger with timestamp, campaign id, script pack version, and user-id/ephemeral-id And the "Start Calling" control remains disabled until acknowledgments are recorded
Cross-Platform Context Parity (Web, iOS, Android)
Given the same SeatPass is redeemed on Web, iOS, and Android clients When operational context is applied Then display name, script pack version, region, language, and campaign metadata match across platforms And time-to-ready (from link open to dialer enabled) is ≤ 5 seconds on broadband and ≤ 10 seconds on 4G And platform-specific fallbacks still enforce seat/breakout locking and idempotent resume And any context discrepancies are logged with platform tag and surfaced to the QA dashboard
SeatPass Expiry And Revocation Handling
Given a SeatPass that is expired or revoked When a user attempts redemption Then redemption is blocked with HTTP 401/403 and error code TOKEN_EXPIRED or TOKEN_REVOKED And no seat/breakout is claimed and no session context is applied And audit logs capture the denied attempt with token id, user-id/ephemeral-id, and reason And the UI offers "Request new SeatPass" and disables retry without a new token
Pre-Scoped Permissions & Least-Privilege Access
"As an admin, I want pre-scoped permissions for alternates so that temporary access is limited to only what is necessary for the shift."
Description

Provide permission templates specifically for SeatPass alternates that constrain capabilities to call execution and necessary read access only. Enforce time-bounded, scope-limited access to contacts, scripts, notes, and dialer actions with no export, admin, or bulk operations allowed. Restrict PII visibility to the minimum required and segment data access by campaign/region. Automatically revoke permissions on expiration or manual revoke and prevent privilege escalation. Applies consistently across API, mobile, and web surfaces.

Acceptance Criteria
Time-Bounded SeatPass Access for Alternates
Given a SeatPass link with an expiration timestamp T and assigned scope S When an alternate authenticates using the link before T Then a session is created tagged with scope S and a server-side expiry at T And all endpoints return data only within scope S When the current time reaches or exceeds T Then any active sessions created by the SeatPass are revoked within 60 seconds And all subsequent API requests return 401 (expired) and UI sessions are forced to logout within 60 seconds When the SeatPass link is accessed after T Then no session is created and the user is shown "SeatPass expired" with no data exposure When an admin manually revokes the SeatPass before T Then the link becomes unusable immediately and any active sessions are revoked within 60 seconds across web, mobile, and API
Least-Privilege Dial-Only Permissions
Given a SeatPass alternate is in an assigned dialer seat or breakout When the alternate performs actions Then they can: view assigned script(s), view scoped contact records, place/receive calls, log a disposition, and add a single-call note And they cannot: access admin/settings, edit roles, edit scripts, create/delete contacts, global search, export, reports, list/segment editing, or any bulk operation And hidden UI controls for prohibited actions do not render And any direct navigation or API attempt to a prohibited action returns 403 and is logged without side effects
Scope-Limited Data by Campaign and Region
Given a SeatPass alternate is assigned campaign C and region R When the alternate queries contacts, scripts, notes, dialer queues, or call history Then only records tagged with campaign C and region R are returned When the alternate attempts to access any resource outside C or R via UI or API Then the request returns 404 (not found) or 403 (forbidden) with no data leakage And cross-campaign or cross-region aggregates, counts, and leaderboards are not displayed
PII Minimization and Field-Level Redaction
Given a SeatPass alternate views a scoped contact record When the record is rendered in UI or returned by API Then only fields on the SeatPass-Min allowlist are shown/returned And all non-allowlisted PII fields are omitted or masked (e.g., •••) When the alternate requests a non-allowlisted field explicitly via API Then the response excludes the field and returns 400 with a validation error naming the disallowed field And historical notes render but automatically redact non-allowlisted PII tokens
No Export, Admin, or Bulk Operations
Given a SeatPass alternate is authenticated When the alternate navigates to areas with export, admin, or bulk actions Then export/download controls (CSV, Excel, API export endpoints) are absent and server endpoints return 403 on invocation And admin/settings routes are hidden and return 403 on direct access And bulk operations (bulk assign/update/delete, multi-select actions) are disabled in UI and return 403 if invoked via API And no background job is enqueued from any prohibited request
Cross-Surface Enforcement Consistency (API, Mobile, Web)
Given a SeatPass alternate uses the same link to access web, mobile, and API When executing the same allowed and prohibited actions on each surface Then allowed actions succeed consistently across all surfaces with identical scope and data And prohibited actions are uniformly blocked with the same HTTP status codes (API) and UI messaging (web/mobile) And there is no surface where broader data or actions become available And automated parity tests for a representative matrix of 20 actions pass across all three surfaces
Privilege Escalation Prevention and Attribution/Consent Protection
Given a SeatPass alternate is active When the alternate attempts to modify their role, scope, display name, script pack, or region Then the UI presents no such controls and any API call to change these returns 403 When the alternate attempts to modify consent flags or attribution fields on a contact Then fields are read-only in UI and PATCH/PUT requests to these fields return 403 And all calls and notes created by the alternate are recorded with actor=alternate and credit attributed per Attribution Shield to the original owner in the Consent Ledger And security tests confirm that token exchange, refresh, or impersonation flows cannot elevate the alternate beyond the SeatPass-Alternate role
Attribution Shield & Consent Ledger Integration
"As a campaign lead, I want attribution and consent recorded correctly when an alternate uses a SeatPass so that credit and compliance remain accurate."
Description

Record actions taken via SeatPass so that impact credit and compliance remain intact. Maintain distinct fields for credited_to (original assignee), performed_by (alternate), and created_by (system/organizer) and propagate to donations, signups, calls, and notes. Write opt-in/opt-out outcomes, call dispositions, and consent timestamps to the Consent Ledger with correct actor context. Update Impact Board metrics to attribute outcomes to the correct person or team per policy. Prevent data leakage to alternates outside scoped entities while preserving a full audit trail for compliance.

Acceptance Criteria
Propagate actor fields on SeatPass-created records
Given an alternate uses a valid SeatPass link to create or update a donation, signup, call log, or note When the record is saved Then credited_to equals the original assignee, performed_by equals the alternate, and created_by reflects the system/organizer initiating the SeatPass handoff; none of these fields are null Given the action updates an existing record not originally created via SeatPass When the alternate saves changes Then performed_by is set to the alternate, credited_to remains the original assignee per policy, and created_by is not overwritten Given related child objects are created as part of the action (e.g., call -> note) When the parent action is saved Then actor fields are propagated to the child where applicable and are retrievable via API as read-only fields
Consent Ledger writes with correct actor context
Given a call disposition and consent outcome (opt-in/opt-out) are recorded via SeatPass When the alternate submits the outcome Then a Consent Ledger entry is written with performed_by, credited_to, created_by, disposition code, consent timestamp in org timezone and UTC, and source = SeatPass, and the entry passes schema validation Given an alternate attempts to write consent for an entity outside their SeatPass scope When the request is made Then the system returns 403 Forbidden and no Consent Ledger entry is created Given an organizer records a consent action on behalf of an alternate via SeatPass tools When the action is submitted Then the Consent Ledger entry records created_by = organizer, performed_by = alternate (if they executed the action) or system (if automated), and includes a reason_code
Impact Board attribution policy enforcement
Given outcomes recorded via SeatPass exist When Impact Board metrics are calculated Then conversion/credit KPIs accrue to credited_to (person/team) and productivity KPIs (calls placed, notes written) accrue to performed_by, with no double-counting and totals matching underlying record counts Given multiple alternates perform actions credited to the same person/team within a reporting period When the Impact Board renders Then rollups per person/team match the sum of underlying records within 0 discrepancy across all displayed metrics Given an attribution policy toggle (e.g., credit to team vs individual) is changed at time T When the board recalculates Then records timestamped before T retain prior attribution and records at or after T follow the new policy
Scoped access prevents data leakage for alternates
Given an alternate enters via a SeatPass scoped to region R, script pack S, and seat D When they browse, search, or query via UI/API Then only entities within R and S and assigned to seat D are returned; attempts outside scope return 403/404 and are audit-logged Given the alternate opens a record within scope When viewing actor-related fields Then actor identifiers outside the alternate’s scope are not exposed beyond allowed display names; raw IDs and cross-entity links outside scope are suppressed Given the alternate attempts to traverse links to related entities outside scope When the navigation or API follow occurs Then no PII or metadata is leaked and the response does not confirm existence beyond a generic authorization error
Immutable audit trail for SeatPass activity
Given any create or update occurs via SeatPass When the system records the event Then an append-only audit entry is written including event_type, performed_by, credited_to, created_by, seat_id, seatpass_id, UTC timestamp, and before/after field snapshots with a checksum Given an admin attempts to modify or delete an audit entry When the action is performed Then mutation is blocked; only approved PII redaction is allowed and that redaction is itself appended as a new audit entry with reason_code and actor Given auditors query audit entries for a record impacted by SeatPass When results are returned Then the sequence is complete and ordered with no gaps and the count equals the number of persisted mutations on the record
SeatPass expiration and revocation behavior
Given a SeatPass is expired or revoked When an alternate attempts to create or update any record or write to the Consent Ledger Then the request is rejected with 401/403, no partial writes occur, and an audit entry records the rejection with reason = expired_or_revoked Given a SeatPass expires during an in-flight submission When the user submits the form Then the operation fails atomically with an error prompt to re-authenticate; zero new records or ledger entries are created Given a SeatPass is reissued for the same seat and credited_to When the alternate resumes work Then new actions carry the same credited_to unless explicitly changed by an organizer, and historical records remain unchanged
External reporting and API reflect actor fields and protections
Given an admin exports donations, signups, calls, and notes When the export is generated Then each row includes credited_to_id, performed_by_id, created_by_id, and seatpass_id with consistent data types and referential integrity to user/team tables Given an alternate client attempts to set credited_to, performed_by, or created_by via API When the request is processed Then the server ignores client-supplied values and returns 422 IMMUTABLE_FIELD if modification is attempted; server populates actor fields based on context Given a compliance connector reads Consent Ledger entries When records are fetched Then actor context fields and timestamps conform to the published schema and pass integrity checks; any PII redactions preserve ledger linkage and do not break joins
Handoff Audit, Revocation & Monitoring
"As an organizer, I want visibility and the ability to revoke and audit SeatPass links so that I can manage risk and respond quickly to issues."
Description

Capture a complete lifecycle trail for each SeatPass, including creator, intended seat, delivery channel, first open, redemption, session duration, and expiration. Provide organizer tools to view status, revoke links instantly, regenerate with new TTL, and invalidate all outstanding passes for a shift. Emit alerts for suspicious patterns (multiple redemption attempts, geo anomalies) and surface analytics on usage and success rates. Export audit logs for compliance reviews. Ensures operational oversight and rapid issue resolution.

Acceptance Criteria
Lifecycle Audit Trail Captured for Each SeatPass
- When a SeatPass is created, the system records: seatpass_id (UUIDv4), creator_user_id, intended_seat_id, permission_scope_hash, delivery_channel, created_at (ISO 8601 UTC). - On first open, the system appends an event with first_open_at, source_ip, user_agent, and geo (country/region). - On redemption, the system appends an event with redeemed_at, source_ip, user_agent, geo, and redemption_channel. - On session end or timeout, the system appends session_end_at and session_duration_secs. - On expiration or administrative action, the system appends expired_at and reason ∈ {ttl_expired, revoked, bulk_invalidate, regenerated}. - Event writes are immutable and append-only; no existing event is modified; p95 event write latency ≤ 2s. - All timestamps are ISO 8601 UTC with millisecond precision; missing required fields cause the event to be rejected and retried. - Each SeatPass’s full lifecycle is retrievable by seatpass_id via API and UI and matches exactly the events recorded (zero mismatches).
Instant Single-Pass Revocation Enforcement
- Given an organizer revokes a SeatPass, subsequent open/redemption attempts return HTTP 410 with error_code=seatpass_revoked. - Any active session for the revoked SeatPass is terminated p95 ≤ 30s with a visible end-state message and reason=revoked captured in the audit trail. - The SeatPass status chips in UI update to Revoked p95 ≤ 5s for all organizer viewers. - An audit event is appended with actor_user_id, seatpass_id, revoked_at, and method=manual. - No new sessions can be initiated using the revoked link/token after revocation time (enforced at edge and API).
Regenerate SeatPass with New TTL
- When an organizer regenerates a SeatPass with TTL=X minutes, a new token is issued with expires_at=now+X and the prior token is invalid immediately. - Display name, script pack, region, and permission scope are preserved on the regenerated SeatPass. - Attribution Shield and Consent Ledger references remain intact; credit/compliance attribution does not change. - Audit includes a regenerated event with old_token_id (masked), new_token_id (masked), ttl_before, ttl_after, actor_user_id, regenerated_at. - Opening the old link returns HTTP 410 seatpass_regenerated; opening the new link succeeds within p95 ≤ 5s of action. - Metrics for the original token remain historically visible; new token metrics start fresh.
Bulk Invalidate Outstanding Passes for a Shift
- When an organizer selects Invalidate All for shift=S, all unredeemed SeatPasses for S become invalid p95 ≤ 30s. - Active sessions on S are force-terminated p95 ≤ 60s with reason=bulk_invalidate and a visible notice to alternates. - Subsequent open/redemption attempts on invalidated passes return HTTP 410 with error_code=seatpass_invalidated_shift. - A shift-level audit summary event records actor_user_id, shift_id, invalidated_count, terminated_sessions_count, occurred_at; per-pass invalidated events are appended. - The action is irreversible; previously invalidated passes cannot be reactivated except via regeneration creating new tokens.
Suspicious Activity Detection and Alerting
- The system emits a security alert when any of the following occur: (a) ≥3 failed redemption attempts for the same SeatPass from distinct IPs within 10 minutes; (b) Redemption geo country not in allowed region set for the SeatPass; (c) >5 different SeatPasses redeemed from the same device_id hash within 30 minutes. - Alerts are delivered as in-app notifications p95 ≤ 10s and email to shift owners p95 ≤ 2m, containing seatpass_id(s), pattern type, counts, masked IPs, geos, and timestamps. - Corresponding audit events alert_emitted and alert_resolved are recorded with actor, timestamps, and resolution notes. - Dismissing an alert does not suppress new alerts of the same type for 24h unless explicitly snoozed with a duration; snoozes are audited. - Alert thresholds are configurable per organization and default to the values above.
Organizer Status Dashboard and Analytics
- The dashboard displays, per shift and date range: passes_created, delivered, first_open_rate, redemption_rate, median_session_duration, p50/p90 time_to_first_open, revoke_rate, and invalidation_count. - Metric values reconcile to the underlying audit events within max(0.5%, 1 count) discrepancy. - Data refreshes at least every 60s; a last_updated timestamp is visible. - Filters by shift, organizer, and delivery_channel affect all metrics and the linked event list; CSV export reflects current filters. - Each metric tile links to a drill-down of the exact contributing events (IDs and timestamps).
Audit Log Export for Compliance
- Users with role ∈ {Org Admin, Shift Owner} can export audit logs by date range and/or shift to CSV and JSONL. - Exports include: seatpass_id, event_type, actor_id, shift_id, seat_id, delivery_channel, timestamp, ip_masked (/24), user_agent_hash, geo, reason, and event_checksum. - For ranges ≤100k events, export completes p95 ≤ 60s; larger ranges stream in pages of 10k events with continuation tokens. - Each export is itself audited with actor, parameters, format, row_count, duration, and checksum. - Files include a SHA-256 checksum (and optional PGP signature) that validates against the file contents.
Seat Conflict Resolution & Fallback Routing
"As a shift lead, I want the system to handle seat conflicts and route alternates to a fallback breakout so that no one is blocked from joining the shift."
Description

Guarantee reliable placement when the targeted seat is unavailable by implementing atomic seat locking, concurrency-safe redemption, and deterministic fallback to the designated breakout or alternate pool. Provide organizer-configurable rules (queue, overflow breakout, or deny) and notify stakeholders of the final placement. Preserve all applied context during fallback and ensure user sees clear in-flow messaging. Prevents join failures and maintains productivity during peak times.

Acceptance Criteria
Atomic Seat Locking on Concurrent Redemption
Given two or more alternates redeem the same SeatPass within a 1-second window When the redemption requests are processed Then exactly one request acquires the seat lock and is placed into the targeted seat And all other requests receive a fallback decision according to the organizer rule within 2 seconds And no double-assignment occurs in any system (database, dialer, or breakout roster) And the lock auto-releases within 100 ms of final placement or after a 5-second TTL if placement fails And an audit/Consent Ledger entry records lockId, winner userId, and each loser’s fallback outcome
Deterministic Fallback Routing per Organizer Rule
Given the organizer has configured fallback behavior as one of: queue=AltPoolA, overflowBreakout=Breakout-X, or deny When the targeted seat is unavailable or the seat lock is not acquired Then routing follows the configured rule deterministically without randomization And overflow places the user into Breakout-X with a success confirmation And queue enqueues the user FIFO and returns an integer position >= 1 And deny returns a non-placement with reason code SEAT_UNAVAILABLE_DENY And all outcomes persist finalPlacement.destination and reasonCode in the event log
Context Preservation During Fallback Placement
Given the SeatPass defines displayName, scriptPackId, regionId, permissionScope, attributionId, and consentRef When fallback routing occurs (overflow or queue) Then the final placement carries all defined context unchanged And the UI shows the applied displayName, script pack, and region immediately on join And Attribution Shield credits attributionId for subsequent actions in the session And Consent Ledger links the session to consentRef with no missing or conflicting consents
Final Placement Notifications and In-Flow Messaging
Given an alternate redeems a SeatPass resulting in seat placement or fallback When final placement is determined Then the alternate sees an in-flow banner within 2 seconds indicating destination (seat, breakout, or queue) and reason And the organizer receives an in-app notification within 5 seconds with userId, destination, applied rule, and timestamp And, if outbound channels are enabled, an email or Slack notification is delivered within 60 seconds And notification content excludes sensitive data beyond configured fields and meets accessibility guidelines
SeatPass Link Expiry and Single-Use Enforcement
Given a SeatPass link with TTL=15 minutes and singleUse=true When the first successful placement occurs Then subsequent redemption attempts with the same link are blocked with message "Link already used" and code LINK_CONSUMED And when a redemption occurs after TTL expiry, it is blocked with message "Link expired" and code LINK_EXPIRED And blocked attempts produce no placement and are logged as failed redemption without altering attribution And tokens are unguessable (>=128-bit entropy) and are rejected over insecure transport
Peak Load Performance and Reliability SLOs
Given 1000 concurrent redemption attempts across 200 targeted seats over 60 seconds When the system processes these requests Then 99.5% of placements or deterministic denials complete in <= 3 seconds end-to-end And 100% maintain single-assignment guarantees (zero duplicates) And 5xx error rate is < 0.5% with retries not causing double placement And monitoring emits placementLatencyP95, doubleAssignCount, fallbackBreakdown, and queueDepth, with alerts on SLO breach
Queue Promotion and FIFO Integrity
Given fallback rule=queue for a specific seat with queue=AltPoolA When that seat becomes available Then the next user in FIFO order acquires the seat via atomic lock within 2 seconds And the promoted user is notified in-flow immediately; remaining users see updated queue positions And users can leave the queue; departure removes them and compacts positions without skipping others And if a queued user’s link expires before promotion, they are removed with reason QUEUE_LINK_EXPIRED and notified
Share & Reminder Workflow
"As a coordinator, I want built-in sharing and reminders for SeatPass links so that alternates receive the link promptly and show up on time."
Description

Offer a streamlined UI to create and share SeatPass links via SMS, email, or messaging apps with templated, localized copy and auto-included expiration details. Support contact picker, clipboard copy, and QR code options. Send optional reminders before expiry and notify organizers on successful redemption or failure. Track delivery and click-through where channels support it. Ensures alternates receive actionable invitations and improves show-up rates with minimal organizer effort.

Acceptance Criteria
Multi-Channel Share with Localized Template and Expiration
Given an organizer opens Share & Reminder for a SeatPass And selects SMS, Email, or a Messaging App as the channel When the organizer taps Send Then a unique SeatPass link with an expiry timestamp is generated And the message body is assembled from the selected locale’s template (fallback to English if unavailable) And the expiration date/time is included in human-readable form aligned to the recipient’s timezone when known, otherwise the organizer’s And the handoff to the selected channel succeeds without app crash or freeze And a send confirmation or handoff success state is displayed within 5 seconds
Contact Picker, Validation, and De-duplication
Given the organizer opens the contact picker When selecting contacts from device contacts and GiveCrew People Then duplicate recipients are de-duplicated by normalized phone/email And contacts lacking the selected channel (e.g., no mobile for SMS) are flagged inline with a reason And up to 50 recipients can be selected in one send operation And if contact permissions are denied, manual entry remains available and functional And per-recipient channel availability (SMS/Email/Messaging) is indicated prior to send
Clipboard Copy and QR Code Share
Given the organizer views Share options When Copy Link is tapped Then the SeatPass URL is copied to clipboard and a toast "Link copied" appears within 1 second And the URL contains no PII and includes only a secure token and expiry parameters When Generate QR is tapped Then a QR code encoding the SeatPass URL is displayed with the expiration timestamp beneath And the QR is scannable by iOS and Android stock cameras at 30–60 cm under typical indoor lighting And after expiry, the QR view indicates expiration and disables Save/Share actions
Scheduled Reminders Before Expiry
Given reminders are toggled on with default times at 24 hours and 1 hour before expiry And the invite has not been redeemed When a reminder window is reached Then the system sends the reminder via the original channel (or SMS fallback if original is unavailable) within ±2 minutes of the scheduled time And no more than one reminder is sent per configured time per recipient And reminders are automatically canceled if redemption occurs prior to the send time And reminder content includes updated time-to-expiry and the same SeatPass link
Organizer Notifications on Redemption and Send Failures
Given an invite has been sent When the recipient redeems the SeatPass successfully Then the organizer receives an in-app notification within 30 seconds (or on next app open) including recipient, channel, and timestamp When a delivery or send failure occurs (bounce, undeliverable, expired link) Then the organizer is notified with a failure reason code and a suggested next action (resend, alternate channel, extend expiry) And the Invite activity log is updated with the event and timestamp
Delivery and Click-Through Tracking
Given a channel that supports delivery and/or click tracking When messages are sent and recipients click the SeatPass link Then delivery status (queued, sent, delivered, failed) and click events are recorded with timestamps And telemetry is visible in the Invite detail view per recipient And channels without tracking support are labeled "Not tracked" And tracking is suppressed where consent is not present, per Consent Ledger settings
Localization, Templates, and Length Limits
Given the organizer’s locale is supported When composing the message Then all template variables ({display_name}, {expires_at}, {link}) are resolved without placeholders And date/time is formatted per locale, including timezone abbreviation And right-to-left languages render correctly in preview And if the locale is unsupported, English templates are used And the UI shows an SMS segment counter and warns when the message exceeds 3 segments; email subject length stays ≤ 78 characters

Instant Brief

On join, alternates get a 60‑second micro-brief: today’s goals, script highlights, compliance reminders, and a one-tap test call. A quick “Ready” check marks them as active and updates the roster. Cuts warm-up time, improves confidence, and boosts call quality from minute one.

Requirements

Auto-Trigger Brief on Join
"As an alternate volunteer, I want the brief to start automatically when I join so I can get up to speed without searching for information."
Description

Automatically launch the Instant Brief when a user identified as an Alternate joins an active campaign or shift. Detect join events from invite acceptance, QR sign-up, or roster add, and bind the brief to the correct campaign context (goals, script set, compliance pack) based on the user’s shift, location, and timezone. Gate by role (Alternates only), ensure idempotency (no retrigger within a configurable window), and fall back gracefully if prerequisites (campaign data, connectivity) are missing. Support mobile web and native app with deep-linking from notifications. Log start/end events for analytics and audit; expose hooks to pause/cancel if a supervisor intervenes.

Acceptance Criteria
Auto-Trigger on Join (Invite, QR, Roster Add)
Given an active campaign with an open shift and a user flagged as Alternate And the user joins via invite acceptance, QR sign-up, or roster add When the join event is received by the system Then the Instant Brief is launched automatically within 3 seconds And the join_source is recorded as one of [invite_acceptance|qr_signup|roster_add] And the Instant Brief view is presented as the topmost screen without requiring additional taps
Correct Campaign Context Binding
Given a user assigned to shift S for campaign C with script set V, compliance pack P, location L, and timezone T When the Instant Brief launches Then the brief context object includes {campaignId:C, shiftId:S, scriptSetId:V, compliancePackId:P, location:L, timezone:T} And all time displays and deadlines are rendered in timezone T And goals and script highlights are loaded for campaign C and script set V And no data from any other campaign or shift is displayed
Role Gating (Alternates Only)
Given a user whose role is Alternate When the user joins an active campaign or shift Then the Instant Brief auto-triggers Given a user whose role is not Alternate (e.g., Primary, Supervisor, Admin) When the user joins an active campaign or shift Then the Instant Brief does not auto-trigger And a brief_skipped_not_alternate event is logged with the userId and role
Idempotency Window (No Retrigger)
Given the retrigger suppression window W is configured to 60 minutes And an Alternate triggers an Instant Brief at time t0 When additional join events for the same user and campaign occur at times t1..tn within W minutes of t0 Then the Instant Brief does not re-open And a brief_suppressed_idempotent event is logged for each suppressed event with reference to the original brief_start When a new join event occurs after W minutes from t0 Then the Instant Brief opens again as normal
Graceful Fallbacks and Retry
Given prerequisites are missing or unavailable (e.g., no connectivity, campaign context missing, script set not published) When a join event is processed Then the app does not crash and no blank screen is shown And a non-blocking fallback screen appears stating "Brief unavailable" with a user-friendly reason And a brief_start_failed event is logged with error_code, userId, campaignId (if known), shiftId (if known) And an automatic retry is scheduled with exponential backoff up to 3 attempts within 10 minutes And a manual Retry button is available when connectivity is restored And the roster is not marked Active until the brief is successfully completed
Cross-Platform Deep Linking (Mobile Web and Native)
Given a push or in-app notification contains a deep link to the Instant Brief for campaign C and shift S When the user taps the notification on a device with the native app installed Then the native app opens directly to the Instant Brief with context {C,S} within 3 seconds When the native app is not installed Then the mobile web client opens to the Instant Brief with the same context {C,S} within 4 seconds And the deep link preserves join_source=notification And the user can complete the brief without additional authentication prompts if already authenticated
Analytics Logging and Supervisor Controls
Given an Instant Brief starts Then a brief_start event is logged with {userId, campaignId, shiftId, join_source, platform, timestamp_utc} When the user taps Ready Then a brief_complete event is logged with the same identifiers and duration_ms And the roster status for the user is updated to Active within 2 seconds When a supervisor issues Pause from the supervisor console Then the brief is hidden within 2 seconds And a brief_paused event is logged with {userId, campaignId, shiftId, supervisorId} And the roster remains Not Active When a supervisor issues Cancel Then the brief is terminated without marking the roster Active And a brief_cancelled event is logged with {userId, campaignId, shiftId, supervisorId} And no auto-retrigger occurs unless a new join event is received after any configured suppression window
Dynamic Brief Composer
"As an alternate volunteer, I want a concise, up-to-date brief of today’s goals and script changes so I know exactly what to say from minute one."
Description

Assemble a 60‑second micro-brief from live campaign data: today’s targets and goals, top three script highlights (diff from last session), and required compliance reminders. Use a templating engine with versioning and localization, pulling content from Campaign Goals, Script Revisions, and Compliance Policies objects in GiveCrew. Enforce a strict timebox with progress indicator and progressive disclosure (headlines first, expandable details). Provide synchronized text and optional audio narration, with accessible formatting (WCAG AA: captions, readable fonts, contrast). Cache per-user brief variants for offline continuity while ensuring content invalidates on upstream changes. Telemetry captures completion, time spent per module, and drop-off points.

Acceptance Criteria
Live Data Micro‑Brief Assembly
Given a logged-in user starts the Instant Brief When the Dynamic Brief Composer assembles content Then the brief includes exactly: today’s numeric targets and goals from Campaign Goals, the top three script highlights that differ from the user’s last session from Script Revisions, and all required Compliance Policies reminders Given fewer than three changed script highlights exist since the user’s last session When selecting highlights Then the remaining slots are filled with the highest-priority current highlights and the brief still shows exactly three highlights Given the user has no prior session When composing the brief Then the brief shows the current top three script highlights and labels them as new Given live data updates are available at composition time When composing the brief Then the latest published versions are used and each module is stamped with its source object version ID and timestamp
Templating, Versioning, and Localization
Given a brief template with version ID and translations for en-US and es-ES exists When composing for a user whose locale is es-ES Then the es-ES variant is rendered, and any missing keys fall back to en-US without placeholder artifacts Given a template or translation is updated to a new version When composing after the update Then the new template version ID and translation version IDs are used and recorded in composition metadata Given composition metadata is inspected When viewing the record Then it contains template_version, script_revision_version(s), compliance_policy_version(s), and locale code for auditability
60‑Second Timebox, Progress Indicator, and Progressive Disclosure
Given the user starts the brief When the first headline appears Then a visible countdown timer starts at 60 seconds in mm:ss format and cannot be paused Given the brief is in progress When the user expands any headline to view details Then the timer continues and the progress indicator updates to reflect module completion state (Goals, Script, Compliance) Given the timer reaches 0 When time expires Then the brief content becomes read-only, a time’s-up state is shown, and telemetry records completion_state=timed_out Given the user views all required headlines (and any mandated details) before time expires When the last required item is acknowledged Then the brief records completion_state=completed and displays remaining time
Synchronized Text and Optional Audio with WCAG AA Accessibility
Given the user taps Play on audio narration When narration starts Then the corresponding text segment is highlighted in sync, captions are displayed, and playback controls are operable via keyboard Given the brief UI is rendered When evaluated for accessibility Then all text meets WCAG 2.1 AA contrast (>= 4.5:1), default font size is >= 16px, focus order is logical, and all interactive elements have accessible names and roles Given captions are toggled When the user changes the setting Then captions are on by default and can be turned off, and a full transcript is available without audio Given the OS reduce-motion preference is enabled When progress indicators animate Then motion is minimized in accordance with the preference
Per‑User Offline Cache and Invalidation on Upstream Changes
Given the brief is successfully composed while online When caching occurs Then a per-user, per-campaign brief variant is stored with source content version IDs and a composed_at timestamp Given the device is offline and a cached variant exists When the user starts the brief Then the cached variant is displayed with a visible Last updated timestamp and no network calls are made Given upstream content changes in any source object (Goals, Script, Compliance) When the device reconnects Then the cache is invalidated if any source version ID differs, and a fresh brief is composed before the next start Given no cached variant exists and the device is offline When the user attempts to start the brief Then an offline-unavailable message is shown and telemetry records start_blocked_offline
Telemetry: Completion, Module Timing, and Drop‑Off Capture
Given a brief session starts When events are emitted Then the system records: brief_started, module_entered, module_exited (with module identifiers), and either brief_completed or timed_out; if the session ends without completion, a drop_off event is recorded with the last module Given a session completes When telemetry is inspected Then it includes total_duration_ms, duration_ms per module (Goals, Script, Compliance), template_version, and locale Given temporary network loss occurs during the session When telemetry cannot be sent immediately Then events are queued locally and sent on reconnect without loss or duplication
Resilience and Data Source Error Handling
Given a transient failure occurs when fetching Campaign Goals or Script Revisions When composing the brief Then the brief renders available modules, displays a non-blocking warning for each missing module, and telemetry records missing_modules with identifiers Given Compliance Policies cannot be loaded When composing the brief Then the brief does not start, an actionable error message is shown to the user, and telemetry records error_code=compliance_unavailable Given a previously missing module becomes available before the brief ends When data arrives Then the module content is inserted without resetting the timer, and the progress indicator updates accordingly
One‑Tap Test Call
"As an alternate volunteer, I want a one-tap test call so I can confirm my audio setup and caller ID before calling real contacts."
Description

Provide a single-tap action within the brief to place a test call via the integrated dialer/VoIP, validating microphone, network quality, and caller ID configuration before live outreach. Play a short whisper prompt and echo test, confirm audio path, and display pass/fail with troubleshooting tips. If VoIP permissions are denied, fall back to native dialer with a test line. Record outcomes (latency, jitter, pass/fail), allow a quick re-test, and keep the flow within the 60‑second brief budget. Surface results to the Ready check to block activation on hard failures (configurable). Store diagnostics for support and quality tracking.

Acceptance Criteria
One-Tap Test Call Starts and Completes Within Brief Window
Given the user is viewing the Instant Brief When they tap "Test Call" Then the integrated VoIP test initiates within 1 second And the call connects or fails within 7 seconds And a whisper prompt plays within 2 seconds of connect And the full test completes and a result (Pass/Warning/Fail) is shown within 20 seconds of tap And a "Retest" option is available immediately after the result And the test runs in-brief (no context switch) unless native dialer fallback is used
Audio Path Verified via Whisper Prompt and Echo Test
Given the test call is connected When the whisper prompt plays Then audio is heard on the selected output device When the echo phase begins and the user speaks for up to 3 seconds Then the system detects returned audio above noise floor by at least 10 dB and not clipped And the round‑trip echo is detected within 800 ms And the UI shows "Audio path confirmed" or targeted troubleshooting tips if detection fails
Network Quality Measured and Graded with Thresholds
Given the test call is active When network metrics are sampled for at least 5 seconds Then RTT latency <= 250 ms, jitter <= 30 ms, and packet loss < 3% yields "Pass" And any of latency 251–500 ms, jitter 31–60 ms, or loss 3–5% yields "Warning" with tips And any of latency > 500 ms, jitter > 60 ms, or loss >= 5% yields "Fail" And the displayed result includes the measured values
Outbound Caller ID Configuration Verified
Given an outbound caller ID is configured When the test line reports the observed caller ID Then it matches the configured E.164 number And if there is no match or no caller ID configured, the result is "Fail" with guidance to set/verify caller ID And when native dialer fallback is used, the observed number is read back and compared to configuration
Permissions Handling and Native Dialer Fallback
Given microphone and VoIP/call permissions are required When permissions are missing or denied Then the app prompts for permission or provides a settings deep link And if VoIP permission remains denied, a "Call Test Line via Phone" fallback is offered and launches the native dialer with the test number And upon returning from fallback, the app records the attempt and reason as "Fallback Used" and displays an outcome And if microphone permission is denied, the result is "Fail" with steps to enable it
Ready Check Gated by Hard Failures (Configurable)
Given the org setting "Block activation on test call hard failures" is enabled When the most recent test result is "Fail" for audio path, caller ID, or network Then the "Ready" action is disabled and an inline reason is displayed And when a subsequent test result is "Pass" or an admin override is applied, the "Ready" action becomes enabled And when the setting is disabled, a warning banner is shown but "Ready" remains enabled
Diagnostics Logged for Support and Quality Tracking
Given a test call attempt completes with any outcome When results are saved Then the record includes timestamp, user, device/OS, network type, latency, jitter, packet loss, echo success, observed vs configured caller ID, permissions state, fallback used, and outcome (Pass/Warning/Fail) And the record is available to admins within 1 minute And logs are retained for at least 30 days and exportable via CSV or API
Ready Check & Roster Sync
"As an alternate volunteer, I want to mark myself Ready and be slotted automatically so I can start helping right away without waiting for a coordinator."
Description

After completing the brief and test call, display a prominent Ready control that, when confirmed, updates the user’s status to Active and immediately syncs with the roster and assignment engine to place them into open calling slots. Ensure the transition is atomic and idempotent, with optimistic UI and rollback on errors. Notify supervisors and shift leads in real time; update queues, capacity, and coverage on the Impact Board. Provide an Undo within 15 seconds and auto-timeout if no activity follows. Respect eligibility (training completed, compliance acknowledged) and produce audit logs (who, when, version of brief).

Acceptance Criteria
Ready Control Visibility and Eligibility Gate
Given a user has completed the Instant Brief and test call in the current session And the user's required training is completed and compliance acknowledgment is current When the brief completion view loads Then a prominent Ready control is displayed and enabled Given the user lacks required training or current compliance acknowledgment When the brief completion view loads Then the Ready control is disabled or hidden And a clear eligibility reason is shown to the user Given eligibility changes between brief completion and Ready tap When the user taps Ready Then server-side eligibility is re-validated and blocks activation if ineligible And the UI remains Not Ready and shows the blocking reason
Atomic Activation and Roster Placement
Given an eligible user taps Ready When the system processes the activation Then the user's status is set to Active and an open calling slot is assigned in a single atomic transaction And if slot assignment fails for any reason, the user's status reverts to Not Ready and no slot is held And no partially Active state is externally observable in roster feeds or assignments
Idempotent Activation and Retries
Given the user taps Ready multiple times or the network retries the activation request within a 60-second window When the backend receives duplicate activation intents for the same session Then only one Active status change occurs and only one slot is assigned And duplicate supervisor notifications and Impact Board updates are not emitted And subsequent duplicate calls return a success response without additional side effects
Optimistic UI with Undo
Given an eligible user taps Ready Then within 500 ms the UI displays an Activating state and exposes an Undo control And the Undo control remains available for 15 seconds from the first tap When the user taps Undo within 15 seconds Then the user's status is reverted to Not Ready, any held/assigned slot is released, and the UI reflects the reversal within 2 seconds And supervisors/Impact Board are updated to reflect the reversal within 5 seconds And if activation had already completed, Undo still performs the full reversal consistently
Real-time Roster, Notifications, and Impact Board Updates
Given a user becomes Active via Ready Then supervisors and shift leads receive a real-time notification within 5 seconds And roster queues and capacity reflect the change within 3 seconds And the Impact Board coverage and capacity metrics update within 5 seconds Given an activation rollback or Undo occurs Then reverse notifications and updates occur within the same SLAs
Inactivity Auto-timeout After Activation
Given a user becomes Active via Ready When no call is initiated and no assignment work begins within the configured idle threshold (default 2 minutes) Then the system auto-times out the user to Not Ready And any held/assigned slot is released And roster queues, capacity, and the Impact Board are updated within 5 seconds And supervisors are notified of the timeout within 5 seconds And the user UI shows a clear timeout message with a path to re-activate
Activation Audit Logging
Given the user taps Ready When the activation completes, is undone, fails, or auto-times out Then an immutable audit log entry is written capturing who (user ID), when (UTC timestamp), action (activate/undo/fail/timeout), and brief version identifier And each log entry includes the activation request ID for traceability And logs are queryable by supervisors within 1 minute of the event and exportable in CSV
Compliance Acknowledgment Capture
"As a compliance manager, I want recorded acknowledgments for required reminders so we can demonstrate adherence to regulations and protect the organization."
Description

Present mandatory compliance reminders within the brief and require explicit acknowledgment tied to the current policy version before enabling Ready. Record user ID, timestamp, policy/version IDs, and campaign context to a tamper-evident audit trail. Re-prompt on version change or after a configurable interval (e.g., 30 days). Provide exportable reports for audits, and surface noncompliance blocks to supervisors. Support regional variants (e.g., TCPA consent language) based on user locale and campaign type. Ensure data minimization and access controls for privacy.

Acceptance Criteria
Ready Button Gated by Current Policy Acknowledgment
Given a user opens Instant Brief for a campaign with compliance policy version V When the compliance reminders are displayed and the user has not acknowledged V Then the Ready control is disabled and a message indicates acknowledgment is required to proceed Given the user explicitly checks the acknowledgment for version V and taps Ready When the system validates the acknowledgment maps to V and the current campaign context Then the user is marked Active, and the Ready transition completes Given the policy version updates to V+1 while the brief is open When the user had previously acknowledged V Then Ready becomes disabled again and V+1 reminders are shown, requiring a new acknowledgment Given the user unchecks or cancels acknowledgment When they attempt to tap Ready Then the Ready action is blocked and no active status change occurs
Tamper‑Evident Audit Trail Recording
Given an acknowledgment for compliance policy version V is submitted When the system records the event Then an append‑only audit record is created containing: user_id, policy_id, policy_version=V, campaign_id, campaign_type, user_locale, timestamp (UTC ISO‑8601), acknowledgment_status=accepted And the record includes a cryptographic hash of its contents and the previous record’s hash to form a verifiable chain Given audit records exist for a date range When chain verification is executed Then the system returns a valid continuity result with no breaks; any mutation or deletion causes verification to fail Given a duplicate acknowledgment attempt for the same user, policy_version, and campaign context within 5 minutes When processed Then the audit trail stores a single idempotent record or marks duplicates as no‑ops without altering the chain integrity
Re‑Prompt on Version Change or Interval
Given a user last acknowledged policy version V at timestamp T0 When they open Instant Brief and the configured re‑ack interval (default 30 days) has elapsed since T0 (UTC) Then the system requires a fresh acknowledgment before enabling Ready Given a user previously acknowledged version V When the active policy version for the campaign changes to V+1 Then the user is re‑prompted and Ready remains disabled until V+1 is acknowledged Given an organization admin updates the re‑ack interval configuration When a user next opens Instant Brief Then the new interval is used to determine whether re‑prompting is required
Supervisor Visibility of Noncompliance Blocks
Given a volunteer is blocked from Ready due to missing/expired acknowledgment When a supervisor opens the roster view for the campaign Then the volunteer appears with a visible "Compliance hold" badge and reason, within 10 seconds of the block Given a supervisor filters the roster by compliance status When selecting "Compliance blocked" Then only users currently blocked for acknowledgment are listed with counts per campaign Given a supervisor attempts to mark a blocked user Ready When acknowledgment is missing Then the system prevents the override and offers an action to re‑prompt the user to acknowledge
Regional Variant Selection and Tracking
Given a user’s locale and the campaign type are known (e.g., en-US + phone banking) When the compliance reminders are rendered Then the system displays the correct regional variant (e.g., TCPA consent language) with its policy_id and version identifier Given the required regional variant is unavailable When rendering Then a safe default variant is shown with a distinct version identifier and an error is logged for remediation Given an acknowledgment is recorded When writing to the audit trail Then the record includes the region/variant identifier used at display time
Audit Report Export for Compliance
Given a compliance admin selects a date range, campaign(s), and policy version(s) When requesting an export Then the system generates downloadable CSV and JSON within 60 seconds (or streams if >50k rows) containing: user_id, timestamp, policy_id, policy_version, campaign_id, campaign_type, user_locale, acknowledgment_status, and chain verification data (e.g., root hash) Given an export is generated When downloaded Then the action is logged with actor_id, timestamp, filters, and file metadata in an admin audit log Given the export contains user identifiers When viewed by roles without compliance-admin permission Then access is denied and no file is produced
Data Minimization and Access Controls
Given the system stores compliance acknowledgment data When inspecting stored fields Then only the minimum required fields are present: user_id, timestamp, policy_id, policy_version, campaign_id, campaign_type, user_locale, acknowledgment_status; no names, emails, phone numbers, or script content are stored in the audit trail Given role-based access control is configured When a Volunteer or standard Organizer attempts to view detailed acknowledgment records Then access is denied; only aggregated statuses (e.g., Ready/Blocked) are visible to non-privileged roles Given a Compliance Admin or Supervisor accesses acknowledgment records When permitted Then data visibility matches least-privilege: Supervisors see status and reason per user; Compliance Admins see full audit entries; all access attempts are logged
Interruptions & Resume Flow
"As an alternate volunteer, I want to resume the brief where I left off if I’m interrupted so I don’t waste time repeating steps."
Description

Persist brief progress locally so users can resume seamlessly after interruptions (incoming calls, app backgrounding, connectivity loss). Prefetch brief assets for offline resilience, queue telemetry for later sync, and send a gentle push nudge if the brief is abandoned for more than 5 minutes. Auto-expire stale sessions after the shift window, and provide a quick restart. Ensure state consistency across devices if the user switches phones, resolving conflicts by most-recent activity. Keep the total resume-to-ready path under 20 seconds for a fast return.

Acceptance Criteria
Resume After Interruption Within 20 Seconds
Given the user is in the Instant Brief during an active shift with assets prefetched, When the app is interrupted by an incoming call, OS app switch, or temporary connectivity loss ≤ 10 minutes, Then on return the brief reopens to the exact last step with inputs preserved and media position restored within 2 seconds. Given the user returns the app to foreground from background, When they tap Continue, Then the path from foreground to a tappable Ready control is ≤ 20 seconds (P95) and requires no re-login or re-navigation. Given the interruption occurred while offline, When the user resumes, Then no network calls block progression and cached assets are used for all remaining steps.
Offline Prefetch of Brief Assets
Given a user taps Join to start the Instant Brief, When the first step is displayed, Then all brief assets (text, images, audio, scripts, checklists) have been downloaded and cached for offline use. Given device connectivity drops before or during the brief, When the user advances steps or plays media, Then all content renders from cache without errors, missing media, or blocking spinners. Given a new shift starts, When the user opens the brief, Then cached assets from prior shifts are invalidated and the latest assets are fetched before step 1.
Queued Telemetry and Reliable Sync
Given the device is offline during the brief, When the user completes steps or interactions, Then telemetry events are enqueued locally with ordered sequence numbers and local timestamps. Given connectivity is restored, When the app is foregrounded, Then queued events are uploaded in order within 10 seconds and de-duplicated server-side via stable event IDs. Given an upload attempt partially fails, When retries occur, Then exponential backoff is used and sending continues until success or 24 hours elapse, without creating duplicate records.
Abandonment Push Nudge at 5 Minutes
Given the user leaves the Instant Brief screen or is idle on it, When 5 minutes of inactivity elapse and notifications are permitted, Then exactly one gentle push notification is sent prompting resume and is canceled if the user returns before 5:00. Given the user has already tapped Ready in the current shift, When the 5-minute timer would fire, Then no nudge is sent. Given one nudge was sent in the current shift, When additional abandonments occur, Then further nudges are suppressed for that shift.
Auto-Expire and Quick Restart After Shift Window
Given the shift window has ended including a 15-minute grace period, When the user attempts to resume an in-progress brief, Then the session is marked expired and a one-tap Restart Brief option is shown. Given the user taps Restart Brief after expiration, When the brief loads, Then a fresh session begins with the latest assets, prior inputs are discarded, and the old session is finalized with status=expired. Given an expired session exists, When the user attempts to mark Ready without restarting, Then the action is blocked and the user is directed to restart.
Cross-Device State Consistency and Conflict Resolution
Given the user starts the brief on Device A and opens it on Device B in the same shift, When both devices are online, Then Device B loads the latest server state within 5 seconds and resumes at the most recently completed step. Given conflicting edits occur on Devices A and B, When the server reconciles state, Then most-recent activity timestamp wins and only one Ready state is recorded. Given Ready is marked on one device, When the other device is opened, Then it reflects Ready within 5 seconds and prevents duplicate submissions.
Brief Effectiveness Analytics
"As a program manager, I want to measure how the Instant Brief affects readiness and call quality so I can iterate on content and improve outcomes."
Description

Collect and visualize KPIs tied to the Instant Brief: start/completion rate, time-to-ready, test call pass rate, and first-hour quality proxies (e.g., connection rate, error flags, script adherence prompts). Segment by campaign, shift, and user cohort (new vs. returning alternates). Feed summary stats to the Impact Board and expose a dashboard for program managers. Support A/B testing of brief variants and export to CSV/BI. Enforce role-based access and anonymization for volunteer-level drill-down where required.

Acceptance Criteria
KPI Event Collection for Instant Brief Sessions
Given an alternate joins and opens Instant Brief, When the brief screen loads, Then a brief_started event with user_id, campaign_id, shift_id, and timestamp is logged within 1 second. Given the alternate taps Ready, When the brief completes, Then a brief_completed event with timestamp is logged and time_to_ready_s is computed as completed_at − started_at. Given the alternate initiates the test call from the brief, When the call ends, Then a test_call_result event is logged with pass true|false and a failure_reason when pass is false. Then daily aggregates for start rate, completion rate, median and p90 time_to_ready_s, and test call pass rate are computed and stored with campaign_id, shift_id, and cohort tags.
First-Hour Quality Proxies Computation
Given an alternate is marked Ready, When the first 60 minutes elapse, Then the system computes first_hour_connection_rate = connected_calls/dial_attempts for that window. Given compliance errors occur within the first hour, When an error is raised, Then error_flags_count is incremented for that window. Given script adherence prompts trigger within the first hour, When a prompt is shown, Then script_prompt_count is incremented for that window. Then first-hour metrics are available at alternate, shift, and campaign rollups within 10 minutes after the window ends.
Segmentation by Campaign, Shift, and Cohort
Given a program manager opens the analytics dashboard, When a campaign, shift, and cohort filter (cohort: New = first Ready; Returning = Ready count > 1) are applied, Then all KPI tiles and charts update to the selected segment within 2 seconds for datasets ≤ 100k records. Given multiple filters are combined, When filters are applied, Then only records matching all filters are included and counts/rates reconcile with raw totals. Given no filters are applied, When the dashboard loads, Then it shows the last 7 days across active campaigns by default. Given a filter yields no data, When the view renders, Then a zero-state appears with no PII and an option to clear filters.
Impact Board Summary Stats Feed
Given active campaigns today, When hourly summaries run, Then the Impact Board feed includes per-campaign start rate, completion rate, avg time_to_ready_s, test call pass rate, and first_hour_connection_rate. Given the Impact Board fetch fails, When a retry policy executes, Then up to 3 retries occur with exponential backoff and failures are audit-logged. Then Impact Board values match dashboard aggregates for the same window within ≤ 0.5% absolute difference. Then new summary values appear on the Impact Board within 5 minutes of summary completion.
A/B Testing for Instant Brief Variants
Given two or more brief variants are active, When an eligible alternate joins, Then a variant is assigned via weighted randomization (default 50/50) and persisted for 30 days to ensure consistent exposure. Given KPIs are recorded for a session, When events are stored, Then each event includes variant_id for attribution. Given the A/B tab is opened, When each variant has ≥ 50 completed briefs, Then per-variant start rate, completion rate, median time_to_ready_s, test call pass rate, and first_hour_connection_rate are displayed with sample sizes. Given a test is paused, When new joins occur, Then no new assignments are made while existing assignments remain valid for analysis and export.
CSV and BI Export of Analytics
Given a user with export permission applies dashboard filters, When Export CSV is requested, Then a CSV downloads containing one row per alternate-session with columns: date, campaign_id, shift_id, cohort, variant_id, brief_started_at, brief_completed_at, time_to_ready_s, test_call_pass, first_hour_connection_rate, error_flags_count, script_prompt_count. Given the filtered dataset exceeds 200k rows, When export is requested, Then a background job generates the file and emails a secure link within 15 minutes. Given a BI client calls the Analytics API with a valid API key, When querying with filters, Then paginated JSON is returned matching the CSV schema and respecting access controls. Then all timestamps are ISO 8601 UTC; rates are decimals [0,1] with 3 decimal places; column headers use snake_case.
Access Control and Anonymization for Analytics
Given roles {Admin, Program Manager, Volunteer}, When accessing the analytics dashboard, exports, or API, Then only Admin and Program Manager are authorized and Volunteers receive HTTP 403. Given a non-Admin user drills down to volunteer-level data, When rows are displayed or exported, Then names, emails, and phone numbers are omitted or masked and a stable pseudonymous volunteer_id is shown instead. Given an Admin views volunteer-level data, When rows are displayed or exported, Then full PII fields are visible. Then all access attempts and data exports are audit-logged with user_id, timestamp, resource, action, and outcome; anonymization rules are enforced uniformly across UI, CSV, API, and Impact Board feeds.

Target Reflow

When someone drops, their remaining targets automatically reassign to available alternates based on priority tiers and do-not-contact rules—no duplicates, no gaps. Captains can lock sensitive lists or set a fair-split mode. Ensures high-value targets get covered without manual list shuffling.

Requirements

Reflow Trigger Detection
"As a field captain, I want dropped assignments to trigger automatic reflow of remaining targets within seconds so that high-priority contacts are not left uncovered."
Description

Detect drop events (manual cancellation, no‑show flag, unassignment, or shift decline) and automatically compute the remaining target inventory tied to the dropped assignee. Initiate a reflow job within 5 seconds, with debounce and batching to avoid thrash when multiple changes occur. Scope the reflow to the relevant campaign, shift, and list segment, honoring existing assignment models and time windows. Implement idempotent processing so repeated triggers do not create duplicate actions. Provide resilience against transient data changes with retries and safe fallbacks.

Acceptance Criteria
Detect Drop Event Types
Given an assignee with active targets When a captain cancels the assignment Then a drop event is emitted with type=manual_cancellation Given an assignee is flagged no-show at shift start When the flag is saved Then a drop event is emitted with type=no_show Given an assignee is removed from a shift roster When the unassignment is saved Then a drop event is emitted with type=unassignment Given an assignee declines a shift invitation When the decline is saved Then a drop event is emitted with type=decline Then each drop event includes campaignId, shiftId, listSegmentId, assigneeId, eventId, occurredAt and is persisted and published exactly once
Compute Remaining Target Inventory
Given an assignee has 27 assigned targets with 7 completed, 3 marked do-not-contact, and 2 outside the valid time window When a drop event occurs Then remainingInventory=15 and excludes completed, do-not-contact, and out-of-window targets And the remaining inventory is derived only from the dropped assignee’s assignments and reflects current data at processing time And the computation completes within 200 ms for up to 500 assigned targets
Enqueue Reflow Within 5 Seconds With Debounce
Given a drop event occurs When processed Then a reflow job is enqueued within 5 seconds of event occurredAt And if multiple drop events for the same scope (campaignId+shiftId+listSegmentId) occur within a configured debounce window of 2 seconds Then only one reflow job is enqueued and it uses the latest state And the job payload includes campaignId, shiftId, listSegmentId, assigneeIds, remainingInventoryCount, assignmentModelId, schedulingWindow And at most one active reflow job exists per scope concurrently
Batch Multiple Drops Per Scope
Given 5 assignees from the same campaign, shift, and list segment drop within 2 seconds Then exactly one reflow job is enqueued for that scope containing all 5 assigneeIds and the combined remaining inventory Given drops occur across different shifts or list segments in the same campaign Then separate reflow jobs are enqueued per unique scope Then batched processing preserves per-assignee inventory attribution in the job payload for downstream allocation
Scoped and Model-Aware Reflow Initiation
Given a drop event for campaign A, shift S1, segment L When a reflow job is created Then it affects only campaign A, shift S1, segment L and does not touch other campaigns, shifts, or segments And the job carries the correct assignmentModelId and enforces the valid scheduling window for targets included And if the list segment is locked by a captain Then the system records an audit event and defers reflow for that scope without enqueuing a job
Idempotent Processing of Repeated Triggers
Given duplicate delivery or reprocessing of the same drop eventId within 10 minutes When handled Then at most one reflow job is created and no duplicate actions occur And reprocessing produces the same persisted state and metrics (idempotent) and records a deduplication hit And idempotency holds across service restarts and message redelivery
Resilience With Retries and Safe Fallbacks
Given a transient database or queue error occurs during inventory computation or job enqueue When detected Then the system retries up to 3 times with exponential backoff starting at 200 ms And if all retries fail Then the system records an error, emits an alert, and schedules a safe retry job 1 minute later without duplicating effects And operations are atomic: either the job is enqueued and inventory snapshot recorded, or no side effects are committed
Priority-Based Alternate Selection Engine
"As an organizer, I want alternates chosen based on defined target priority tiers and agent fit so that the most important contacts are reassigned to the best-suited people first."
Description

Rank and select alternates using configurable target priority tiers and agent fit criteria (skills, tags, proximity, language, and availability). Traverse tiers to ensure the highest-value targets are reassigned first, with deterministic tie-breakers for repeatable outcomes. Support pluggable weighting rules and per-campaign configurations, including rule previews for admins. Integrate with roster availability and capacity limits to ensure only eligible alternates are selected. Expose metrics on selection outcomes for tuning priority policies.

Acceptance Criteria
Priority Tier Traversal and Ordering
Given a dropped agent with remaining targets across tiers T1 > T2 > T3 And at least one eligible alternate exists in each tier When the selection engine runs Then all T1 targets are reassigned before any T2 targets And all T2 targets are reassigned before any T3 targets And the engine does not assign any lower-tier target while any higher-tier target remains unassigned with at least one eligible alternate And no selected alternate violates availability or capacity limits
Deterministic Tie-Breakers and Repeatability
Given a fixed input dataset, configuration, and deterministic seed And candidate alternates for a target have identical composite scores When the engine is executed twice with the same inputs and seed Then the tie-breaker sequence [least recent assignment, shortest distance, lowest agent_id] is applied in that order And both runs produce identical ranked lists and assignment outcomes And the run output captures the seed and tie-breaker decisions per assignment
Fit Criteria Scoring and Eligibility Filters
Given a campaign configuration with weights skills=40%, language=30%, proximity=20%, tags=10% and language marked as required And a target requiring skills=[phonebank], language=[Spanish], tags=[youth] When the engine scores alternates within a 15-mile radius and overlapping availability window Then only alternates meeting mandatory requirements (availability and required language) are eligible And each eligible alternate receives a composite score per configured weights And alternates missing a mandatory criterion are excluded with an explicit reason And the highest-scoring eligible alternate is selected for the target without duplicates
Per-Campaign Weight Rule Configuration and Preview
Given an admin updates weighting rules and required/optional flags for a campaign When they open Rule Preview for a sample target Then the preview displays per-criterion score contributions, total composite score, inclusion/exclusion reasons, and rank for at least 10 candidate alternates And adjusting a weight updates preview scores and ranks within 1 second without saving And saving creates a new configuration version with timestamp and author and makes it active for subsequent runs
Roster Availability and Capacity Enforcement
Given alternates have defined availability windows and per-day capacity limits When selecting an alternate for a target scheduled within a specific time window Then only alternates with overlapping availability and remaining capacity are considered And existing assignments are respected to prevent over-allocation And if no eligible alternates exist in the current tier, the engine advances to the next tier and logs the skip reason
Outcome Metrics and Observability
Given a reflow run completes When metrics are requested for that run_id via the admin dashboard or metrics API Then the response includes: total targets processed, assigned, unassigned, assignments by tier, exclusion reason counts, average and p90 candidate scores, time-to-assign per target, and number of tie-breakers applied And metrics are stored with configuration version and seed And metrics become available for review within 5 minutes of run completion
No Duplicate Assignments Across Reflow
Given multiple dropped targets share overlapping alternate pools When the engine processes assignments (including concurrent runs) Then no alternate is assigned more than once to the same target And no target receives more than one alternate assignment And uniqueness is enforced at commit time to prevent race-condition duplicates
Do-Not-Contact and Policy Compliance
"As a data steward, I want reflow to automatically exclude do-not-contact records and restricted agents so that we remain compliant and avoid harmful outreach."
Description

Enforce do‑not‑contact and suppression rules across global, campaign, and list levels during reflow. Validate each candidate reassignment against per-contact opt-outs, legal compliance flags (e.g., GDPR/CCPA), time-of-day rules, and agent-level restrictions. Block and log any non-compliant reassignment with reason codes, and continue processing remaining targets. Allow authorized roles to apply documented overrides with audit entries. Maintain compliance reports and expose counts of suppressed reassignments.

Acceptance Criteria
Global/Campaign/List DNC Enforcement During Reflow
Given a reflow is triggered due to an assignee drop And alternate agents are available When the system evaluates each target-contact pair for reassignment Then it must check global, campaign, and list-level DNC/suppression rules for the contact and intended channel And if any DNC/suppression applies, the candidate assignment is not created And the system continues evaluating remaining candidates for that target until candidates are exhausted And no reassignment exists for any contact on a DNC/suppression list for the evaluated channel
Legal Compliance Flags Block Reassignment
Given a target-contact has one or more legal compliance flags that prohibit outreach for the intended purpose or channel (e.g., GDPR/CCPA/legal_hold) When the reflow attempts to assign this target to an alternate agent Then the reassignment is blocked And the reason code "LEGAL_FLAG" is recorded for the blocked event And the system continues processing the next candidate without interruption
Time-of-Day Contact Restrictions Enforcement
Given organization time-of-day windows are configured per channel and resolved by the contact’s timezone And the current time is outside the allowed window for the target-contact and channel When the reflow evaluates a candidate reassignment Then the reassignment is blocked for that contact and channel And the reason code "TIME_WINDOW" is recorded And the system continues evaluating other candidates and targets And when the current time is within the allowed window, the same target-contact passes this check
Agent-Level Restrictions and Locked Lists Respected
Given an alternate agent has restrictions (e.g., clearance level, language/region limits, tag exclusions) or the list is locked by a captain When the reflow considers assigning a target-contact to that agent or from that list Then the reassignment is blocked if it violates agent restrictions or list lock And the reason code is recorded as "AGENT_RESTRICTED" or "LIST_LOCKED" respectively And the system continues to the next eligible agent or leaves the target unassigned if no eligible agent remains
Non-Compliant Reassignment Logging with Reason Codes
Given any reassignment is blocked by a compliance rule When the block occurs Then the system creates a log entry containing: reflow_run_id, target_id, contact_id, channel, rule_code, rule_name, timestamp (UTC), actor=system, and context (campaign_id, list_id, agent_id considered) And counters per rule_code and per channel are incremented atomically And the blocked event is queryable in compliance reports and APIs within 5 seconds of occurrence
Authorized Override with Audit Trail
Given a blocked reassignment exists And a user with an authorized role (e.g., Captain or Compliance Officer) initiates an override When the user provides a mandatory justification note and selects a scoped override (single target-contact, list, or time-bound) Then the system validates permissions and scope, records an audit entry (actor_id, timestamp, scope, before/after policy state, justification) And re-evaluates and applies the reassignment if now compliant under the override And if the user is unauthorized, the attempt is denied with no data change and an audit entry is recorded for the denied attempt
Compliance Reports and Suppression Counts Exposed
Given one or more reflow runs have completed When a user views the Compliance Report for a campaign or date range Then the report displays counts of suppressed reassignments by rule_code, channel, campaign, and agent And totals equal the sum of underlying logged events for the same filters (tolerance = 0) And users can export the suppressed reassignment dataset with all logged fields for the selected filters
Duplicate-Free Coverage and Atomic Assignment
"As a captain, I want reflow to assign each target exactly once and keep total coverage intact so that there are no duplicates or missed contacts."
Description

Guarantee each target is assigned exactly once and that overall coverage goals are maintained during reflow. Use transactional locking or optimistic concurrency with retries to prevent race conditions and double-assignments across concurrent jobs. Validate current assignment state at commit time and roll back partial changes on failure. Track and surface metrics for duplicates prevented and gaps filled, with automated reconciliation for eventual consistency. Provide safeguards to avoid assignment churn by respecting minimal dwell times.

Acceptance Criteria
Concurrent Reflows Yield Unique Assignments
Given a pool of targets with existing active assignments And two or more reflow jobs are triggered concurrently against overlapping targets When all jobs complete Then each target has exactly one active assignment And no assignee holds the same target more than once And no commit results in duplicate rows for the same target_id with active=true And the final assignment count equals the number of targets eligible for assignment
Atomic Rollback on Mid-Job Failure
Given a batch reflow has started for multiple targets And a runtime error occurs after some provisional updates are staged When the job terminates Then the pre-reflow assignment state remains unchanged And zero partial updates are persisted And a single audit log entry records the failure with reason and zero committed updates
Optimistic Concurrency Retries Resolve Version Conflicts
Given versioned assignment records and max_retries=3 And a commit conflict is detected at write time When retries execute with exponential backoff (100ms, 200ms, 400ms) Then exactly one commit succeeds and produces a valid duplicate-free state And if conflicts persist after max_retries, the job fails gracefully with no changes persisted and an error is logged And the retry count for the run is recorded in metrics.retries
Coverage Goals Maintained Post-Reflow
Given coverage goals of 100% for Tier 1 targets and 95% overall And one or more assignees drop causing gaps When reflow completes Then all Tier 1 targets are assigned if at least one eligible alternate exists for each And overall coverage is at least 95% or equal to the maximum feasible given eligibility constraints And no target with at least one eligible alternate remains unassigned
Minimal Dwell Time Prevents Assignment Churn
Given a minimal dwell time of 48 hours is configured And a target was reassigned less than 48 hours ago When a new reflow runs Then that target is not reassigned again But if an urgent override is set for that target, reassignment is allowed and the reason is captured in the audit log
Automated Reconciliation Ensures Eventual Consistency
Given transient errors or out-of-order updates have occurred When the reconciliation job runs every 5 minutes Then any discrepancies between intended and actual assignments are corrected within 10 minutes And after reconciliation, all targets comply with uniqueness and coverage rules And running reconciliation twice back-to-back yields zero additional changes on the second run (idempotency)
Metrics for Duplicates Prevented and Gaps Filled Are Accurate and Visible
Given a reflow run that encounters potential double-assignments and unassigned targets When the run completes Then metrics.duplicates_prevented equals the number of blocked double-assignment attempts And metrics.gaps_filled equals the number of previously unassigned targets that received an assignment And metrics.gaps_remaining equals the number of targets that had no eligible alternates And these metrics are visible on the Impact Board within 1 minute of job completion
Fair-Split Mode and Load Balancing
"As a captain, I want a fair-split option that spreads work across my team so that no volunteer is overloaded and morale stays high."
Description

Offer an optional fair-split mode that distributes reassigned targets evenly across eligible alternates based on recent load, performance, and per-agent caps. Implement round-robin and weighted distribution strategies while honoring availability, skills, and priority tiers. Allow configuration per campaign or list, including maximum per-agent reflow assignments per window. Provide a preview of distribution impacts before commit and real-time adjustments when alternates accept or decline. Persist settings and expose summary stats on balance quality.

Acceptance Criteria
Enable and Persist Fair-Split Settings per Campaign/List
Given a campaign or list, When a captain enables fair-split mode and selects a distribution strategy (round-robin or weighted), sets per-agent cap and window duration, and applies eligibility filters (availability, skills, priority tiers), Then the settings save successfully, persist across sessions, and are retrievable via UI and API with timestamp and actor. Given campaign-level defaults exist, When a child list overrides any fair-split setting, Then the list-level override applies only to that list and does not change the campaign default. Given the app is reloaded or accessed from another device, When the same campaign or list is opened, Then the previously saved fair-split configuration is pre-populated and used for subsequent reflows.
Round-Robin Reflow Evenness Across Eligible Alternates
Given 60 reflow-eligible targets in the same priority tier and 4 alternates who meet all eligibility filters with identical caps and availability, When reflow runs in round-robin mode, Then each alternate receives either 15 assignments or a distribution that differs by at most 1 assignment due to remainder, and no alternate exceeds their cap. Given ties in eligibility, When ordering alternates for round-robin, Then a deterministic tiebreaker is applied (lowest recent load, then agent ID alphabetical) so that repeated runs with the same inputs produce identical results. Given the batch completes, Then there are no duplicate assignments and no unassigned targets in the batch.
Weighted Distribution by Recent Load and Performance
Given weighted mode is selected with weights load=0.7 and performance=0.3 and eligible alternates have recorded recent load and performance metrics, When a preview for 100 same-tier targets is generated, Then the preview displays the ideal assignment count per alternate based on the configured weights and current caps. When the commit is executed from that preview without any input changes, Then each alternate's actual assignment count matches the preview within +/-1 and no cap is exceeded. Given two alternates with equal performance, When their recent load differs, Then the alternate with lower recent load receives greater than or equal assignments; given equal load, the one with higher performance receives greater than or equal assignments.
Per-Agent Cap Enforcement by Time Window
Given a per-agent cap of 10 assignments per 7-day window at the campaign scope and an alternate has 9 assignments already within the window, When 5 additional targets are reflowed, Then that alternate receives at most 1 additional assignment and the remaining targets are redistributed to other eligible alternates. Given both campaign-level and list-level caps are configured, When computing remaining capacity, Then the most restrictive effective capacity is used to limit assignments. Given the window boundary passes, When new reflows occur, Then counts outside the current window are not considered toward the cap.
Pre-Commit Distribution Preview and Impact Summary
Given a list in fair-split mode, When the user opens the preview, Then the UI shows for each eligible alternate: projected assignment count, percent share, remaining capacity, and reasons for exclusion for ineligible alternates (e.g., skills mismatch, unavailable, DNC, cap reached). When the user commits from the preview, Then an audit record is stored including before/after per-alternate counts, strategy used, weights and caps, actor, timestamp, and batch ID. Given the user cancels the preview, Then no assignments are changed and no audit record is created.
Real-Time Adjustment on Accept/Decline
Given pending reflow offers are sent to alternates, When an alternate accepts an assignment, Then the assignment is marked confirmed and removed from the offer pool and counts/stats update accordingly. When an alternate declines or the offer times out, Then within 10 seconds the target is reflowed to the next eligible alternate according to the current strategy without violating caps, skills, availability, or priority tiers, and the change is reflected in the summary. Given a target is declined by multiple alternates, Then the system does not re-offer the same target to the same alternate more than once within the active window.
Balance Quality Summary Stats Exposure
Given reflow completes (or updates after accept/decline), When viewing the balance quality summary, Then the system displays per-alternate assignment counts, total reflowed, variance, standard deviation, coefficient of variation, Gini coefficient, and counts blocked by caps/eligibility, in the UI and via API. Given subsequent accept/decline events occur, When the summary is refreshed, Then the metrics update to reflect the latest confirmed assignments. Given historical analysis is needed, When querying by batch ID or date range, Then the summary and underlying assignment ledger are retrievable for at least 90 days.
Sensitive List Locking
"As a captain, I want to lock sensitive lists so that high-touch or restricted contacts are not reassigned without approval."
Description

Enable captains to lock specific lists or segments so reflow skips moving those targets while continuing to reassign others. Support role-based and time-bound locks, with mobile controls to set, view, and remove locks. Display lock status and reasons to all affected users and block conflicting changes with clear error messages. Ensure locks are enforced at rule evaluation time and at assignment commit time. Audit all lock modifications with user, timestamp, and justification.

Acceptance Criteria
Role-Based Lock Creation and Visibility
Given a Captain with Lock Manager permission views list X on mobile When they create a role-based lock with scope=List and provide a justification Then the lock is saved and a Locked badge with the reason is displayed on list X and all child targets for all users And only Captains with Lock Manager or Org Admin can edit or remove the lock; other roles see the action disabled or receive E_LOCK_403 And an audit entry is created capturing user, timestamp (ISO 8601), scope, and justification
Enforcement at Evaluation and Commit
Given a scheduled or manual Target Reflow is running while list X is locked When the rules engine evaluates candidate moves Then targets within the locked scope are excluded from evaluation And if any commit attempts to modify a locked target, the transaction aborts with HTTP 409/code E_LOCK_409 and no partial changes are persisted And reflow continues for non-locked targets while maintaining no-duplicate/no-gap constraints
Time-Bound Lock Start and Expiry
Given a time-bound lock on list X with start=now+5m and end=now+2h (org timezone) When a reflow runs before the start time Then the lock is not enforced When a reflow runs between start and end Then the lock is enforced When current time passes end Then the lock status becomes Expired automatically, an audit entry is recorded, and the next reflow includes targets from list X
Mobile Set, View, and Remove Controls
Given a Captain opens list X on mobile When they tap Lock List, enter a reason (min 10 characters), optional end time, and confirm Then the lock is created and a Locked badge with reason appears within 5 seconds When they view Lock Details Then they see scope, created by, created at, reason, start/end times, and who can unlock When they remove the lock and confirm with justification Then the lock is removed, an audit entry is written, and targets in X are eligible in the next reflow run
Conflicting Manual Changes Blocked with Clear Errors
Given a target T belongs to a locked scope When a user without unlock privileges attempts manual reassignment, bulk import, or API update that affects T Then the operation fails atomically with HTTP 409/code E_LOCK_409 and an error message including lock scope, reason, and owner And no changes are applied to T or related assignments
Status and Reason Visibility to Affected Users
Given any user views a locked list, its segment, or a target card within the locked scope Then a visible Locked badge and the reason are displayed in context And initiating any action that would reassign shows a preemptive modal/toast explaining the lock and linking to details
Comprehensive Audit Trail and Protections
Given any lock event occurs (create, edit reason/times, auto-expire, remove) Then an immutable audit record is written with event type, actor (or system), timestamp (ISO 8601), scope IDs, previous and new values, and justification text And audit entries are viewable in the Audit UI with filters by scope and date and can be exported as CSV And attempts by non-admins to delete audit records return E_AUDIT_403 and make no changes
Reassignment Notifications and Audit Trail
"As an organizer, I want clear notifications and an audit trail of reflow actions so that my team stays informed and I can explain or roll back changes if necessary."
Description

Send timely notifications to newly assigned alternates via push, SMS, or email with context, due times, and quick-action links. Notify captains on reflow completion with a concise summary of reassigned counts, exceptions, and any uncovered targets. Record an immutable audit log for each reassignment that includes initiator, trigger type, rule path, and pre/post states; make entries exportable and searchable. Surface reflow metrics to the Impact Board to reflect coverage and response improvements. Provide a short rollback window for captains to undo a reflow batch when necessary.

Acceptance Criteria
Alternate Receives Assignment Notification With Context and Due Time
Given an alternate is newly assigned one or more targets by a reflow batch When the reflow completes Then the alternate is sent a notification within 60 seconds via preferred channel (push; fallback to SMS; fallback to email) And the notification includes campaign name, number of targets, due time, and a secure quick-action link And only one notification is sent per assignee per batch assignment And delivery status is tracked and recorded for audit (delivered, failed, bounced)
Quick-Action Link Enables One-Tap Accept or Decline
Given a recipient opens the quick-action link from a reflow notification When they choose Accept Then all targets in that batch assignment are marked Accepted within 2 seconds and visible in their assignment list And the acceptance event (user, timestamp, batch_id, target_ids) is recorded in the audit log When they choose Decline Then the targets are returned to the available pool for reflow and the decline event is recorded
Captain Receives Reflow Completion Summary
Given a reflow batch completes with reassignment outcomes When the batch status is set to Completed Then the captain receives a summary notification within 60 seconds via in-app and email And the summary includes reassigned count, exception count, uncovered target count, batch_id, trigger type, and start/end timestamps And the summary provides links to view audit log and to initiate rollback (if within window)
Immutable Audit Log For Each Reassignment
Given any target is reassigned by a reflow batch When the reassignment is committed Then an audit entry is written with fields: batch_id, target_id, previous_assignee, new_assignee, initiator, trigger_type, rule_path, pre_state, post_state, timestamp, checksum And audit entries are immutable; any correction creates a new entry referencing the prior entry_id And the audit log is searchable by target_id, assignee_id, batch_id, campaign_id, and timestamp range, returning the first 100 results within 2 seconds for up to 50k entries And audit entries are exportable to CSV and JSON with column headers matching the stored fields and a file checksum
Impact Board Displays Reflow Coverage and Response Metrics
Given one or more reflow batches have run in the last 24 hours When the Impact Board is opened or refreshed Then it displays metrics: post-reflow coverage rate, change vs pre-reflow, median time-to-cover, alternate response rate (24h), and uncovered target count And metrics reflect data within 5 minutes of batch completion And selecting a metric drills down to a filtered view of affected assignments and batches
Rollback Window to Undo a Reflow Batch
Given a reflow batch completed less than 10 minutes ago When a captain initiates Undo on that batch Then all reassigned targets are reverted to their pre-reflow assignees and statuses in one atomic operation And cancellation/reinstatement notifications are sent to affected alternates and prior assignees And a rollback audit entry is recorded with actor, timestamp, reason, and reversed batch_id And if any reassigned target has progressed (accepted or activity logged), those items are excluded from rollback and listed to the captain; the remainder still revert

Timezone Triage

Pings alternates whose local time is within legal calling windows and closest to the target timezone, while respecting quiet hours and opt-outs. Increases pick-up rates, reduces wasted pings, and keeps outreach compliant for distributed, multi-timezone phonebanks.

Requirements

Timezone Resolution & DST Handling
"As a campaign coordinator, I want GiveCrew to infer and maintain accurate local times for contacts and volunteers so that pings and calls align with each person’s real local time."
Description

Automatically determine and maintain accurate local time zones for targets (callees) and volunteer alternates using a prioritized blend of data sources (phone country/area codes, geocoded addresses, and user-profile settings). Normalize all calculations on the IANA time zone database with automatic daylight saving transitions and historical/forward changes. Provide deterministic fallback rules when data is incomplete, background re-resolution when contact data changes, and caching to minimize API calls. Expose a consistent service used by scheduling, pinging, and reporting so that Timezone Triage decisions (who to ping and when) are always based on correct local times across regions.

Acceptance Criteria
Prioritized Timezone Resolution from Multiple Sources
Given a contact with a valid user-profile timezone, When resolving timezone, Then the profile timezone is used if it is a valid IANA ID. Given a contact with a valid geocoded address and no valid profile timezone, When resolving timezone, Then the timezone derived from the address is used. Given a contact with only a valid phone country/area code and no usable profile timezone or address, When resolving timezone, Then the timezone derived from the phone mapping is used. Given a contact with only a country code and no usable area code, address, or profile timezone, When resolving timezone, Then the country default timezone is used. Then the response includes ianaTimeZoneId, sourceUsed (profile|address|phone|countryDefault), confidence (high|medium|low), resolvedAt. Then given identical inputs, multiple calls return the same ianaTimeZoneId.
DST Transitions and IANA Normalization
Given any alias or deprecated zone ID (e.g., US/Pacific), When resolving timezone, Then the service returns the canonical IANA zone ID (e.g., America/Los_Angeles). Given a timestamp during spring-forward gap in the resolved timezone, When computing local time and offset, Then the service marks the local time as invalid and returns the next valid instant with correct UTC offset. Given a timestamp during fall-back ambiguity in the resolved timezone, When computing local time and offset, Then the service chooses the first occurrence by rule and indicates ambiguityResolved=true with offset applied accordingly. Given historical and future dates affected by tzdb changes, When computing offsets, Then offsets match the installed IANA tzdb version for those dates. Then dstInEffect is correct for all test timestamps across at least one full year for each supported timezone (0 mismatches).
Background Re-Resolution on Data Change
Given a contact’s address, phone, or profile timezone is created or updated, When the change is saved, Then a re-resolution job is queued within 5 seconds. Then the contact’s cached timezone entry is invalidated immediately. Then the re-resolution completes within 60 seconds under nominal load and updates ianaTimeZoneId, sourceUsed, and confidence. Then an event timezone.resolved is emitted with before/after fields for consumers. Then transient failures are retried with exponential backoff up to 3 attempts; final failures are logged with errorCode and correlationId.
Caching and External Lookup Minimization
Given repeated resolution requests with unchanged inputs, When called within a 24-hour TTL, Then the service returns the cached result. When input data changes or a re-resolution event occurs, Then the cache entry is invalidated and the next request refreshes the cache. Under load test of 10,000 unique contacts, Then cache hit rate is >= 85% and external lookup calls are reduced by >= 60% versus a no-cache baseline. Then p95 latency is <= 150 ms for cache hits and <= 600 ms for cache misses.
Consistency Across Scheduling, Pinging, and Reporting
Given a set of 1,000 contacts and 5 timestamps each, When scheduling, pinging, and reporting request local times, Then all three modules return identical ianaTimeZoneId and offsets for each case (0 discrepancies). Then all modules call the same timezone service endpoint with versioned API v1. Then the service is idempotent: repeated requests with the same inputs return identical outputs.
Deterministic Fallbacks and Ambiguities
When multiple sources are available, Then the priority order is: profile > geocoded address > phone area code > country default. When a phone area code maps to multiple time zones, Then the tie-breaker is deterministic using a versioned mapping table and alphabetical order of candidate IANA IDs. When no sources are usable, Then the country default is the IANA zone for the most populous region; if multiple, choose alphabetical first; confidence=low. Then the response includes tieBreakerUsed (true|false) and mappingTableVersion.
Auditability and Metadata Exposure
Then each resolution persists metadata: inputsUsed, ianaTimeZoneId, utcOffset, dstInEffect, sourceUsed, confidence, tieBreakerUsed, tzdbVersion, resolvedAt. Given an admin requests timezone metadata for a contact, When calling GET /timezone/{contactId}, Then the latest resolution metadata is returned. Given a reporting export over a date range, When generated, Then it includes timezone resolution metadata for each contact at export time.
Jurisdictional Calling Window Rules Engine
"As a compliance lead, I want jurisdiction-specific calling windows enforced and explainable so that our outreach stays legal across regions."
Description

Implement a rules engine that enforces legal calling windows based on the callee’s jurisdiction (country/state/province/city where applicable) with versioned rule sets, holiday/weekend exceptions, and organization- or campaign-level overrides. Support quiet-hour definitions per jurisdiction and configurable grace buffers. Evaluate windows at decision time (not send time only) and block or queue pings that would violate rules. Provide preloaded defaults (e.g., US 8/9am–9pm variations), bulk import for other regions, and a simulation mode to preview compliance before enabling. All evaluations return structured reasons (allowed/blocked, rule version, jurisdiction matched) consumable by UI and logs.

Acceptance Criteria
Decision-Time Evaluation Blocks Out-of-Window Calls
Given a callee with a resolved jurisdiction and local time zone, When a ping decision is requested at time T, Then the engine evaluates legality using T (decision time), not scheduled send time. Given the current time is inside the allowed window, When evaluated, Then the engine returns allowed=true and action=send with reason.includes('within_window') and includes windowStart, windowEnd, jurisdictionMatched, ruleVersion. Given the current time is outside the allowed window and queueing is enabled, When evaluated, Then the engine returns allowed=false and action=queue with queueUntil equal to the next legal window start adjusted by graceBuffer, and includes reason.includes('outside_window'). Given the current time is outside the allowed window and queueing is disabled, When evaluated, Then the engine returns allowed=false and action=block and includes reason.includes('outside_window'). Given any evaluation, When a response is returned, Then it contains structured fields: allowed, action, evaluationTimestamp (UTC), jurisdictionMatched, jurisdictionPath, ruleVersion, timeZoneUsed, windowStart, windowEnd, exceptionApplied (nullable), overrideApplied (nullable), graceBufferMinutes, and queueUntil (nullable).
Jurisdiction Hierarchy Resolution with Versioned Rule Sets
Given a callee with city, state/province, and country, When evaluated, Then the engine selects the most specific jurisdiction with an active rule (city > county/region > state/province > country) and returns jurisdictionMatched and jurisdictionPath reflecting the resolution. Given multiple rule versions exist for the matched jurisdiction with effectiveAt timestamps, When evaluated at time T, Then the engine applies the latest version whose effectiveAt <= T and returns ruleVersion. Given no jurisdictional rule is found at any level, When evaluated, Then the engine applies the configured global default policy (allow or block) and returns fallbackSource='globalDefault' and policyValue. Given an input with ambiguous or invalid jurisdiction codes, When evaluated, Then the engine rejects with action=block, allowed=false, and reasons including 'invalid_jurisdiction' and a validationErrors array.
Quiet Hours and Grace Buffer Enforcement
Given a jurisdiction defines quietHours (start and end in local time), When evaluated within quiet hours, Then the engine treats the period as disallowed regardless of general calling window and returns exceptionApplied='quiet_hours'. Given a graceBufferMinutes > 0, When evaluated within graceBufferMinutes before window start or within graceBufferMinutes after window end, Then the engine treats the time as outside the window and reflects graceBufferMinutes in the response. Given windows or quiet hours cross midnight, When evaluated at boundary times, Then the engine correctly computes windowStart and windowEnd on the correct calendar day in the callee’s local time. Given jurisdictions observing daylight saving time, When evaluated on DST transition dates, Then timeZoneUsed reflects the correct offset and window calculations align with local legal time.
Holiday and Weekend Exception Handling
Given a jurisdiction defines weekend exceptions, When evaluated on a weekend date, Then the engine applies the weekend window instead of the weekday window and returns exceptionApplied='weekend'. Given a jurisdiction defines a holiday calendar with specific dates or observed rules, When evaluated on a holiday, Then the engine applies the holiday window and returns exceptionApplied='holiday' including holidayId or name. Given both weekend and holiday exceptions could apply, When evaluated, Then the engine applies the more restrictive legal window and returns exceptionApplied including both sources with resolution='most_restrictive'. Given an unknown holiday id is provided in data, When evaluated, Then the engine falls back to the non-holiday rule and includes reason.includes('holiday_not_found').
Organization and Campaign Override Precedence
Given an organization-level override exists and a campaign-level override exists for the same jurisdiction and time, When evaluated, Then the campaign override takes precedence over the organization override, which takes precedence over jurisdiction defaults, and the response includes overrideApplied with level and id. Given an override narrows the window, When evaluated within the narrowed-out period, Then the engine blocks or queues according to configuration and returns reason.includes('override_narrowed'). Given an override widens the window, When evaluated within the widened period, Then the engine allows sending and returns reason.includes('override_widened'). Given override effectiveAt and expiresAt are defined, When evaluated outside that range, Then the override is ignored and the response reflects the underlying jurisdiction rule version.
Preloaded US Defaults and Bulk Import for Other Regions
Given a new tenant in the US, When the rules engine initializes, Then default US state-level windows are present including known 8am/9am–9pm variations and each state rule has a ruleVersion and timeZone references. Given an admin uploads a CSV for another region in bulk import dry-run mode, When processed, Then the engine validates all rows, returns no changes applied, and provides a per-row validation report with counts of passed and failed rows. Given the same CSV is uploaded in apply mode, When processed, Then valid rows are created or versioned updates are applied idempotently, invalid rows are rejected with error details, and a summary includes created, updated, skipped, and failed counts. Given a subsequent identical import, When processed, Then no duplicate versions are created and the summary indicates skipped due to no changes.
Simulation Mode Returns Structured Reasons Without Enforcement
Given a campaign has simulation mode enabled, When evaluations are requested, Then the engine returns wouldBeAction (send/queue/block) and allowedSimulated, includes all structured reasons (allowed/blocked determination, jurisdictionMatched, ruleVersion), and does not block, queue, or send messages. Given simulation mode is disabled, When evaluations are requested, Then the engine enforces the decision (send/queue/block) and does not include wouldBeAction. Given any simulation response, When logged, Then the system emits an audit log entry containing evaluationTimestamp, campaignId, jurisdictionMatched, ruleVersion, windowStart, windowEnd, and wouldBeAction.
Alternate Ranking & Ping Routing
"As a shift captain, I want alternates ranked and pinged automatically based on timezone fit and availability so that open calling slots fill quickly with minimal noise."
Description

Build a ranking pipeline that selects and orders volunteer alternates for each open call slot or call task by filtering to those within legal windows and then scoring on timezone proximity to the target, declared availability, historical response rate, language/skill match, and recency caps. Orchestrate pings in controlled waves with configurable batch sizes, delays, retry logic, and automatic escalation to the next set until the slot is filled or a stop condition is met. Deduplicate across campaigns, prevent ping storms, and emit events for accept/decline so assignments are confirmed in real time. Integrate with existing shift assignment and ‘fill open slots’ flows to minimize manual coordination and reduce wasted pings.

Acceptance Criteria
Legal Window and Opt-Out Filtering
Given a list of volunteer alternates with timezones, jurisdictional calling windows, personal quiet hours, and opt-out flags When the system prepares a candidate pool for a target call slot Then only volunteers whose local time is within both legal calling windows and personal quiet hours are included And all volunteers with global or campaign-level opt-outs are excluded from pings And alternates with missing/uncertain timezone are excluded by default unless allow_uncertain_timezones=true (default=false) And daylight-saving transitions are evaluated using the jurisdiction’s rules for the current date And each excluded volunteer has a machine-readable exclusion reason logged And if no eligible candidates remain, the system returns "No Eligible Alternates" and emits routing.halt.no_candidates
Deterministic Alternate Scoring and Ranking
Given an eligible pool after filtering When the ranking engine scores candidates Then the total score is computed from configurable weights (timezone_proximity, declared_availability, historical_response_rate, language_skill_match) and a recency_penalty And default weights are applied if org-level overrides are not set, and missing components use documented defaults And ties are broken deterministically by last_ping_at ascending, then volunteer_id ascending And candidates hit by a recency cap (e.g., pinged within the last 24h across campaigns) are excluded unless override_recency_cap=true And the system exposes per-candidate score breakdown and reason codes via API for the top N (configurable, default 20) And the final list is stable given identical inputs (idempotent ranking)
Wave-Based Ping Orchestration with Stop Conditions
Given a ranked list and routing settings (batch_size S, inter_wave_delay D, max_waves W, retry_policy R) When the system initiates pings Then the first wave sends to the top S candidates with channel-specific idempotency keys And subsequent waves are triggered after D seconds unless a stop condition is met And stop conditions include: slot filled, W reached, time-to-fill TTL expired, or manual stop And per-recipient retries follow R with exponential backoff and a max attempt cap (default 2) And queued pings for a slot are canceled immediately when the slot is filled And per-wave metrics (sent, delivered, responded, accepted, declined, timed_out) are recorded and exposed
Cross-Campaign Deduplication and Flood Control
Given multiple active campaigns and routing jobs When pings are scheduled for the same volunteer and overlapping timeslots Then the volunteer receives at most one ping for a given timeslot across all campaigns within a rolling window M (configurable, default 60 minutes) And a global rate limit caps pings per volunteer to N per 24 hours (configurable, default 3) And distributed locks or atomic checks ensure only the first job retains the volunteer; others log dedup_source and skip And channel fan-out is limited so the same request does not ping the volunteer on multiple channels simultaneously unless multi_channel=true And all deduplications and rate-limit decisions are audit logged with keys (volunteer_id, slot_id, campaign_id, idempotency_key)
Real-Time Accept/Decline Events and Auto-Confirmation
Given a volunteer receives a ping via app push or SMS When the volunteer responds ACCEPT/YES or DECLINE/NO Then on ACCEPT the slot is reserved and the assignment is created within 2 seconds (p95) with optimistic concurrency controls And all pending pings for that slot are canceled within 3 seconds and the volunteer is marked unavailable for overlapping slots And events are emitted on the event bus: routing.ping_sent, routing.accepted, routing.declined, routing.timeout, routing.canceled with required fields (assignment_id, volunteer_id, campaign_id, slot_id, channel, timestamp, idempotency_key) And the organizer UI reflects the accepted assignment within 3 seconds (p95), with a 30-second polling fallback And on conflicting simultaneous accepts, only one is confirmed; losers receive a courteous "slot taken" message and are not double-booked
Integration with Shift Assignment and Fill Open Slots Flows
Given an organizer starts Fill Open Slots or opens a specific shift When they click Start Timezone Triage Then routing runs without additional configuration using org defaults or saved presets And the UI displays live wave status, current stop condition, and ETA; on completion, the slot shows Filled by Auto-Routing And in Shift Assignment, a View Ranking panel shows the top 20 with score breakdowns and include/exclude reasons And manual override to assign a different volunteer cancels pending pings and updates availability consistently And Undo Auto-Fill reopens the slot and either resumes remaining waves or re-runs routing per organizer choice
Auditability, Metrics, and Configuration Controls
Given a routing run completes or halts When compliance or performance data is requested Then an exportable audit artifact (JSON/CSV) is available with candidate list, inclusion/exclusion reasons, score components, legal window evaluations, timestamps, and idempotency keys And platform metrics are recorded: ping-to-accept rate, average time-to-fill, percent excluded by legal window, rate-limit hits, and cancellation rates, filterable by campaign and timeframe And org-level configuration for weights, windows, rate limits, batch sizes, delays, recency caps, and feature flags is change-audited (who/when/what) and versioned And all defaults are documented and discoverable via API/UI help And disabling the feature flag fully stops new routing while preserving historical audit and metrics
Quiet Hours & Opt-out Enforcement
"As a volunteer, I want my quiet hours and opt-out preferences honored automatically so that I’m not contacted at inconvenient times."
Description

Respect volunteer-level quiet hours, snooze windows, and channel-specific opt-outs across all Timezone Triage communications. Provide frequency caps (per day/week) and global Do Not Disturb with expiry options. Merge suppression inputs from user preferences, staff overrides, and legal requirements (e.g., DNC/TCPA constraints) into a single enforcement layer applied before any ping is queued. Surface clear reasons when a candidate is suppressed so organizers can adjust expectations or scheduling without guessing. Ensure enforcement is consistent across SMS, push, email, and in-app notifications.

Acceptance Criteria
Quiet Hours Enforcement (Local Time)
Given a volunteer has quiet hours set from 21:00 to 08:00 in their profile and current local time is 22:15 And the organizer attempts to trigger SMS, push, email, and in-app notifications via Timezone Triage When the enforcement layer evaluates the contact Then no messages are queued on any channel And the suppression response includes reasonCodes ["QUIET_HOURS"] with reasonDetails including the interval "21:00-08:00" and the volunteer's timezone And nextEligibleAt is set to the next occurrence of 08:00 local time that also falls within the legal calling window
Global Do Not Disturb With Expiry
Given a volunteer has Global DND enabled with expiry 2025-09-01T12:00:00-04:00 When any channel ping is evaluated before the expiry timestamp Then the enforcement layer suppresses the ping with primaryReason "GLOBAL_DND" and includes expiry in reasonDetails And no downstream sender is invoked When a ping is evaluated at or after 2025-09-01T12:00:00-04:00 and no other rules block Then the enforcement layer does not block on DND and allows the ping to be queued
Snooze Window Enforcement And Auto-Resume
Given a volunteer activates a snooze window for 2 hours starting at 14:00 local time When any channel ping is evaluated between 14:00 and 16:00 local time Then the enforcement layer suppresses the ping with primaryReason "SNOOZE_ACTIVE" and reasonDetails include snoozeEnd "16:00" And no messages are queued When a ping is evaluated after 16:00 local time and other rules pass Then the enforcement layer allows the ping to be queued
Channel Opt-Out Honors Allowed Alternatives
Given a volunteer has opted out of SMS but allows email and push And frequency caps and legal windows are not exceeded When Timezone Triage selects a channel to contact the volunteer Then the enforcement layer blocks SMS with primaryReason "CHANNEL_OPT_OUT_SMS" And the system may queue a single message on an allowed channel (email or push) per selection policy And no SMS is sent or queued and delivery metrics record zero SMS attempts
Frequency Caps (Rolling 24h/7d)
Given org caps are configured as globalPer24h=3, globalPer7d=8, smsPer24h=2 And the volunteer has already received 3 total pings (2 SMS, 1 email) in the last 24 hours and 7 total in the last 7 days When an additional SMS is evaluated within the same 24-hour window Then the enforcement layer suppresses the ping with reasonCodes ["FREQUENCY_CAP_GLOBAL","FREQUENCY_CAP_CHANNEL_SMS"] and primaryReason "FREQUENCY_CAP_GLOBAL" And nextEligibleAt equals the timestamp when the oldest of the last 3 pings exits the 24-hour window And no message is queued When an email is evaluated before nextEligibleAt Then the enforcement layer suppresses it with primaryReason "FREQUENCY_CAP_GLOBAL" and no message is queued
Unified Suppression Layer, Priority, and Pre-Queue Check
Given a volunteer matches multiple suppression inputs: on a legal DNC list, under a staff-applied manual suppression until 2025-09-10T00:00:00Z, and currently in quiet hours When any ping is requested by Timezone Triage Then the enforcement layer runs before any queueing and returns suppressed=true And no downstream sender is invoked and no queue entries are created And reasonCodes include ["LEGAL_DNC","STAFF_SUPPRESSION","QUIET_HOURS"] And primaryReason is selected by priority order LEGAL > STAFF_SUPPRESSION > GLOBAL_DND > SNOOZE > QUIET_HOURS > CHANNEL_OPT_OUT > FREQUENCY_CAP And an audit log entry is written capturing requestId, evaluated rules, matched reasons, primaryReason, and timestamp
Legal Calling Window Compliance By Contact Timezone
Given the legal SMS window is configured as 08:00–21:00 local time for the volunteer’s jurisdiction and the volunteer’s current local time is 07:50 When an SMS ping is evaluated Then the enforcement layer suppresses the ping with primaryReason "LEGAL_WINDOW_CLOSED" and reasonDetails include window "08:00–21:00" and localTime "07:50" And no message is queued When evaluated at 08:00 local time and other rules pass Then the enforcement layer allows the SMS to be queued
Multi-channel Ping Delivery & Scheduling
"As an organizer, I want pings sent on the best channel and time for each alternate so that more people accept without being spammed."
Description

Deliver pings via the most effective channel per recipient (SMS, push, email, in-app) with templates that include localized times and one-tap accept/decline links. Schedule sends to land within both the callee’s legal window and the volunteer’s quiet hours, using per-channel throttling, batching, and backoff to avoid spikes. Provide retry and fallback channel rules when a message bounces or is ignored, plus optional A/B timing windows to optimize pickup rates. All deliveries are instrumented with open/click/response events feeding the routing engine and analytics.

Acceptance Criteria
Channel Selection per Recipient
Given a recipient has multiple available channels (SMS, push, email, in-app) and channel-level opt-ins/opt-outs are recorded When a ping is generated Then the system excludes any channel without a valid token/address or with an opt-out or quieted status And selects the initial send channel with the highest 30-day response rate for the recipient; if no history, use org default channel priority And logs selection rationale including candidate channels, scores/priorities, and final choice And sends exactly one initial message via the chosen channel
Localized Times and One-Tap Actions in Templates
Given a ping template contains localized time tokens and accept/decline actions When a message is rendered for recipient TZ=America/Denver and shift at 17:00 recipient local time Then times appear in recipient local timezone with abbreviation (e.g., 5:00 PM MT) And accept and decline links are one-tap deep links containing signed, expiring tokens (>=30 min, <=24h) tied to pingId and recipientId And a successful tap records a response event within 1 second and returns a confirmation screen And expired/invalid links return a safe error and instructions without reserving a slot
Scheduling within Legal Windows and Volunteer Quiet Hours
Given callee timezone, legal calling window rules, and volunteer quiet hours are configured When scheduling a send Then the scheduled timestamp is within the callee’s legal window AND outside the volunteer's quiet hours And if no valid slot exists in the next 24 hours, the ping is held with status=Blocked:NoWindow and an alert is created And all scheduled times are stored in UTC with the source timezones recorded for audit And test fixtures validate daylight saving transitions accurately
Per-Channel Throttling, Batching, and Backoff
Given per-channel throughput limits, batch sizes, and backoff policies are configured When a campaign triggers 10,000 pings Then SMS sends do not exceed the configured rate limit in any rolling 60-second window and are batched per provider rules And email sends are chunked into batches and respect retry with exponential backoff on 4xx/5xx And push notifications coalesce duplicate pings per recipient within a 5-minute window And system metrics show no per-channel burst exceeding 110% of configured limits
Retry and Fallback Channel Rules
Given a message bounces on its initial channel When a bounce event is received Then the system marks the channel as Undeliverable for 24 hours and enqueues a retry on the next eligible fallback channel within 60 seconds And if a message is ignored (no open/click/response) for the configured timeout (default 15 minutes), schedule a fallback send to the next channel And no recipient receives more than one fallback per 60 minutes And all retries/fallbacks maintain the same campaign/pingId linkage for analytics
A/B Timing Windows Experimentation
Given an A/B timing experiment with variants A and B defined as relative windows within the legal calling window When eligible recipients are scheduled Then recipients are randomly and evenly assigned (±2%) to A or B and remain sticky to their variant And sends land within the variant’s target sub-window while honoring legal windows and quiet hours And pickup rate, open, and response metrics are attributed to variant and are exportable And the experiment can be toggled off without affecting already scheduled sends
Delivery Instrumentation and Analytics Feeds
Given deliveries occur across channels When events are emitted (delivered, open, click, bounce, response, unsubscribe) Then each event includes fields: pingId, recipientId, channel, timestamp (UTC), campaignId, and payload metadata And events are delivered to the routing engine within 5 seconds (p95) and persisted idempotently (de-dupe by eventId) And analytics dashboards update within 1 minute (p95) of event receipt And PII is redacted according to policy before export
Admin Configuration & Preview Sandbox
"As an admin, I want to configure rules and preview outcomes before rollout so that I can validate triage behavior and avoid surprises."
Description

Offer an admin UI to configure jurisdictional windows, organizational quiet-hour defaults, ranking weights (timezone distance vs. availability vs. response rate), ping wave cadence, and channel fallbacks. Include a preview sandbox where admins can input a target and a pool of alternates to see the computed eligibility, scores, suppression reasons, and the exact ping schedule before enabling changes. Provide per-campaign overrides with safe-mode rollout (percentage-based) and change-history with revert.

Acceptance Criteria
Configure Eligibility Windows (Jurisdictions + Quiet Hours)
Given an admin sets legal calling windows for US-CA as Mon–Fri 09:00–20:00 and Sat–Sun 10:00–18:00 local time, When the configuration is saved, Then the system validates non-overlapping ranges and persists the windows for US-CA. Given an admin sets organizational quiet hours to 21:00–08:00 local time, When saved, Then preview eligibility evaluates as (within legal window) AND (outside quiet hours) using each alternate’s local timezone. Given the current local time for an alternate in US-CA is 21:30, When preview is computed, Then the alternate is marked Ineligible with suppression reason "Outside legal window (US-CA)". Given the current local time for an alternate is 07:30, When preview is computed, Then the alternate is marked Ineligible with suppression reason "Org quiet hours". Given quiet hours are configured to fully cover 24h or legal windows overlap, When the admin attempts to save, Then the UI blocks save with specific validation messages identifying the conflicting ranges.
Adjust Ranking Weights
Given ranking weights are editable for Timezone Distance, Availability, and Response Rate, When an admin sets weights to 60/20/20 respectively, Then the system enforces that weights sum to 100% and blocks save with an error if not. Given weights 60/20/20 are saved, When the admin runs a preview on the same input pool, Then each alternate shows a composite score recalculated using the new weights and a visible breakdown of component scores. Given two alternates have identical composite scores, When preview is computed, Then ordering is deterministic using tie-breakers (higher Response Rate, then alphabetical by name) and remains stable across repeated previews. Given weights are changed again, When the admin toggles between configurations in preview, Then score changes and ordering update within 1 second of applying the new weights.
Set Ping Wave Cadence
Given an admin sets ping waves to 3, wave spacing to 8 minutes, and wave size to 10 alternates, When saved, Then preview displays three scheduled waves at T+0, T+8, and T+16 minutes with 10 alternates per wave. Given cadence is configured and a scheduled send would fall into an alternate’s quiet hours or outside legal window, When preview is computed, Then that attempt is deferred to the next compliant time and labeled "Deferred to next legal window" with the adjusted timestamp. Given max pings per person per day is set to 2, When preview is computed for a pool where an alternate appears in multiple waves, Then no more than 2 attempts are scheduled for that alternate and additional attempts are suppressed with reason "Daily attempt limit reached". Given an admin sets wave spacing below 1 minute or waves above 10, When attempting to save, Then the UI blocks save with inline validation explaining the allowed ranges.
Configure Channel Fallbacks
Given an admin sets channel fallback order to SMS → Push → Email with a fallback delay of 7 minutes and max 1 attempt per channel, When saved, Then preview shows for each alternate the ordered attempts with timestamps at T+0 (SMS), T+7 (Push), and T+14 (Email). Given an alternate has an SMS opt-out, When preview is computed, Then the SMS attempt is suppressed with reason "Channel opt-out: SMS" and the first attempt is Push at T+0 followed by Email at T+7. Given an alternate lacks compliant consent for all channels, When preview is computed, Then the alternate is marked Ineligible with suppression reason "No compliant channel" and no schedule is produced. Given a channel is duplicated in the configured order or fallback delay is < 1 minute, When attempting to save, Then the UI blocks save with a clear validation error.
Preview Sandbox Eligibility, Scores, and Schedule
Given an admin opens the Preview Sandbox, When they input a target contact and upload/select a pool of alternates, Then clicking "Compute Preview" produces a results table including for each alternate: Eligibility (Yes/No), Suppression Reasons (if any), Composite Score with component breakdown, and the exact ping schedule (timestamps and channels). Given the preview is running, When results are shown, Then no live pings are sent, no outbound logs are created, and the UI displays a "Dry Run: No messages sent" indicator. Given the admin modifies any configuration (windows, quiet hours, weights, cadence, fallbacks), When they recompute preview, Then the table updates to reflect the new configuration and the count of eligible alternates, average score, and total scheduled attempts are recalculated. Given the admin clicks "Export", When the preview has results, Then a CSV is downloaded whose rows and key fields (eligibility, reasons, scores, timestamps, channels) exactly match the on-screen preview for the current configuration.
Per-Campaign Overrides with Safe-Mode Rollout
Given a campaign-specific override is enabled for Campaign X, When the admin sets weights and cadence different from org defaults and sets Safe Mode rollout to 10%, Then preview for Campaign X indicates cohort assignment with 10% of alternates evaluated under the override and 90% under defaults. Given Safe Mode is set for Campaign X, When the admin increases rollout to 50%, Then cohort assignment updates deterministically (sticky by alternate ID) and preview reflects approximately 50% of alternates under the override. Given Campaign X has an override, When previewing another campaign Y, Then org defaults are applied and no override indicators are shown. Given Safe Mode is disabled for Campaign X, When saved, Then 100% of traffic uses the override and preview indicates full rollout.
Change History with Revert
Given an admin saves any configuration change, When the save completes, Then a history entry is recorded including timestamp, actor, affected fields with before/after values, and a version ID. Given multiple prior versions exist, When an admin selects a past version and clicks Revert, Then the active configuration switches to that version, a new history entry is created noting the revert, and preview immediately reflects the reverted settings. Given a non-admin attempts to revert, When they click Revert, Then the action is blocked with an authorization error and no history entry is created. Given the history list is displayed, When an admin views details for a version, Then the system shows a readable diff of key settings (windows, quiet hours, weights, cadence, fallbacks) without allowing edits to historical records.
Audit Logging & Compliance Export
"As a program manager, I want detailed, exportable logs explaining each ping decision so that I can audit compliance and resolve disputes."
Description

Record tamper-evident logs for every triage decision and ping: time, target jurisdiction, legal window calculation, rule version, volunteer preferences applied, selected channel, schedule, and outcomes (accepted/declined/expired). Provide searchable log views with filters and export to CSV/JSON for audits, incident reviews, and regulator inquiries. Include data retention controls and redaction for personal data to meet privacy requirements while preserving compliance evidence. Publish summary metrics to existing reporting so teams can track reduced wasted pings and compliance adherence over time.

Acceptance Criteria
Complete Triage Decision and Ping Log Entry
Given a triage decision results in a ping being sent, When the decision is finalized, Then a log entry is created within 5 seconds containing triage_id, event_time (UTC ISO 8601), target_jurisdiction, legal_window_start, legal_window_end, legal_window_basis, rule_version, volunteer_preferences_applied (quiet_hours window and opt_out status/timestamp), selected_channel, scheduled_at, action=ping_sent, and outcome in {accepted, declined, expired, unknown}. Given a triage decision results in suppression (quiet hours, opt-out, or outside legal window), When processed, Then a log entry is created with action=suppressed, outcome=null, and suppression_reason in {quiet_hours, opt_out, outside_legal_window}, including all fields needed to demonstrate the legal window calculation and preferences applied. Given a log entry is created, Then it is visible via the Logs API and UI list views and is immutable thereafter.
Tamper-Evident Append-Only Log
Given any new log entry is persisted, When stored, Then it is appended with fields entry_hash and prev_hash forming a verifiable hash chain with no ability to update in place. Given the daily integrity verification job runs, When it scans the chain for the prior 30 days, Then it returns integrity_status=pass if and only if no entries were altered or removed; otherwise integrity_status=fail and identifies the first failing triage_id and timestamp. Given a user requests integrity verification for a specified time range up to 1,000,000 entries, When the API is called, Then it returns a signed digest and verification result within 10 seconds.
Search and Filter Log Views
Given a user with Logs:View permission, When they open the log view and apply filters for date_range, target_jurisdiction, action (ping_sent|suppressed), outcome, selected_channel, rule_version, and volunteer_id, Then the results reflect all filters and display total_count. Given a dataset of at least 100,000 log entries, When filtering by any single field or any combination of two fields, Then the first page of results loads in under 2 seconds and supports pagination and sort by event_time and outcome. Given the user selects a display timezone, When viewing results, Then all timestamps render in the chosen timezone with the UTC offset indicated.
Filtered CSV and JSON Export
Given filters are applied in the log view, When the user exports CSV or JSON, Then the exported records exactly match the filtered result set and order at the time the export started. Given an export is generated, Then each record includes the required fields (triage_id, event_time UTC ISO 8601, target_jurisdiction, legal_window_start, legal_window_end, legal_window_basis, rule_version, volunteer_preferences_applied, selected_channel, scheduled_at, action, outcome, suppression_reason if applicable) and the file includes metadata (generated_at UTC, schema_version, filter_summary). Given the filtered result set exceeds 250,000 records, When export is requested, Then the system streams the export in chunks and completes within 10 minutes for up to 1,000,000 records, providing a download link valid for 7 days. Given redaction mode is enabled, When exporting, Then PII fields are masked per the active redaction policy (see PII Redaction) and redaction_applied=true is indicated in metadata.
Data Retention and Legal Hold
Given an organization sets a retention period of N days (30 ≤ N ≤ 730), When the nightly retention job runs, Then log entries older than N days are purged except those flagged legal_hold=true. Given log entries are purged, Then a purge summary entry is created containing time, org_id, cutoff_date, purged_count, preserved_count, and a hash of purged triage_ids, and the summary is visible in the audit trail. Given an admin applies or removes a legal hold on a triage_id or time range, When the retention job runs, Then entries under legal hold are not purged until the hold is removed and the action is itself logged (who, when, scope). Given retention settings are changed, When saved, Then the change is recorded with who, when, old_value, and new_value and takes effect on the next scheduled job.
PII Redaction While Preserving Compliance Evidence
Given redaction mode is Off, When viewing or exporting logs with appropriate permissions, Then all fields are visible and unmasked. Given redaction mode is On, When viewing or exporting logs, Then personal identifiers (phone, email, full_name, volunteer_notes) are masked (e.g., phone shows last 2 digits only) while compliance evidence fields (target_jurisdiction, legal_window_start/end/basis, rule_version, selected_channel, action, outcome, suppression_reason, event_time, scheduled_at) remain fully visible. Given redaction mode is On, When exporting CSV or JSON, Then file metadata includes redaction_applied=true and redaction_policy_version, and no unmasked PII appears in the payload.
Publish Compliance and Efficiency Metrics
Given logs are collected, When the nightly metrics job runs, Then it publishes per-organization, per-day metrics to the existing reporting system: total_pings_sent, suppressed_pings_count, compliant_pings_count, compliance_adherence_rate = compliant_pings_count / total_pings_sent, pickup_rate = accepted / total_pings_sent, and wasted_pings_count = total_pings_sent - compliant_pings_count. Given metrics are published, When recomputed from the raw logs for the same period, Then each metric matches within 0.1%. Given the Impact Board loads, When viewing the compliance panel, Then the latest metrics are visible within 60 minutes of job completion and include 7-day trend deltas for compliance_adherence_rate and wasted_pings_count.

TimeLock Links

Schedule board- or sponsor-only Impact Board links that automatically activate and expire on your chosen window. Extend or revoke in one tap if meetings shift. Prevents lingering access, keeps conversations anchored to the right timeframe, and eliminates manual cleanup.

Requirements

Scheduled Time Window
"As an organizer, I want to set a start and end time for an Impact Board link so that access only happens during the meeting window."
Description

Provide UI and API to create Impact Board links with explicit activation start and expiration end timestamps. Support mobile-friendly date/time pickers, time zone selection (defaulting to org time zone), validation that end > start, and helpful presets (e.g., +30m, +1h, end of meeting). Persist the window on the link record and surface the configured window in the link detail view. Links are uniquely tied to a specific Impact Board and can be regenerated without altering the configured window.

Acceptance Criteria
Create Link with Start and End Times via UI
Given I am on the Impact Board's TimeLock link creation form And I have selected an Impact Board When I enter a valid activation start and expiration end timestamp And I click Create Link Then a new link is created and tied to the selected Impact Board And the configured start and end timestamps are saved on the link record And the board association cannot be changed after creation
Create Link with Start and End Times via API
Given a valid API token with permission to manage the specified Impact Board When I POST to /api/impact-boards/{board_id}/timelock-links with start_at, end_at, and timezone Then the response is 201 Created with the persisted link id, url, start_at, end_at, timezone, and board_id And the link is uniquely associated to the specified board_id And the stored start_at < end_at And the configured window persists across subsequent GET requests
Validate End Is After Start
Given I have entered a start time When I set the end time to be equal to or before the start time Then the Create Link action is disabled and an inline error explains "End time must be after start time" And the API returns 422 with a machine-readable error code end_before_or_equal_start when violated
Time Zone Default and Override
Given my organization time zone is America/Chicago When I open the link creation form Then the time zone selector defaults to America/Chicago And all displayed times reflect the selected time zone When I change the time zone to UTC Then the start and end wall-clock times remain constant while the stored timestamps convert accordingly And the selected time zone is persisted on the link record and returned via API
Mobile-Friendly Date/Time Pickers
Given I am using a mobile device (viewport width <= 480px) When I focus the start or end field Then a native or platform-optimized date/time picker opens And both fields support minute-level precision and ensure accessible labeling And keyboard-only and screen-reader interactions can complete the selection without errors
Helpful Time Presets
Given the link creation form is open with current time 2:00 PM When I tap the +30m preset Then the end time auto-sets to 2:30 PM relative to the current start time When I tap the +1h preset Then the end time auto-sets to 3:00 PM relative to the current start time And an 'End of meeting' preset is available when a meeting end time is present in context and sets end to that time
Persist, Display, and Regenerate Link Without Changing Window
Given a link exists with start_at 2025-08-10T21:00:00Z and end_at 2025-08-10T22:00:00Z When I view the link detail Then the configured window is visibly displayed with time zone context When I tap Regenerate Link Then a new URL token is generated And start_at and end_at remain unchanged on the link record And the prior URL is invalidated while the new URL remains governed by the same configured window
Auto Activation & Expiry Enforcement
"As an organizer, I want links to automatically turn on and off at the right times so that I don’t have to manually manage access or worry about leftover visibility."
Description

Enforce time windows server-side so links only resolve when active and automatically expire at the configured end time. Outside the window, display a friendly, branded message with the next active time (or that access has ended) and contact info. Tokens are invalidated post-expiry to prevent lingering access. Handle clock skew and daylight savings safely, and ensure no cached content is served beyond expiry. All enforcement occurs before any board data is returned.

Acceptance Criteria
Resolve Only Within Active Window
Given a TimeLock link with start S and end E configured in timezone T When a request is made before S Then the server returns a branded access message with the next activation time S in timezone T and contact info And the HTTP response status is 200 and headers include Cache-Control: no-store, no-cache, must-revalidate and Pragma: no-cache And no Impact Board data, API calls, or board assets are returned
Start Inclusive, End Exclusive Time Boundaries
Given a TimeLock link with active window [S, E) in timezone T and server time TS When TS >= S and TS < E Then the link resolves with HTTP 200 and the Impact Board data is returned And response headers prohibit caching with Cache-Control: no-store, no-cache, must-revalidate When TS >= E Then the server returns the access-ended message with contact info and no board data And any attempt to call the board API is blocked server-side
Clock Skew and Daylight Saving Safety
Given a client device has incorrect local time or timezone and a window spans or neighbors a DST change in timezone T When the client requests the TimeLock link near S or E Then activation and expiry are determined by server time converted to timezone T, not client time And activation occurs at the configured local wall time S in T and expiry at E in T across DST transitions And clients with +/-24h local clock skew still see correct enforcement outcomes
Post-Expiry Token Invalidation and Extension
Given a link expires at E and tokens were issued during the active window When any previously issued token is presented after E Then access is denied and the access-ended message is shown and no board data is returned And previously issued tokens cannot retrieve board data via any endpoint When an admin extends the window after E Then previously issued tokens remain invalid and a new token is generated and required for access And extension or revocation propagates globally within 60 seconds
No Cached Content Beyond Expiry
Given the Impact Board was viewed during the active window When the window passes E and the user reloads or navigates back Then no cached board content is rendered and the message page is served after revalidation And CDN/browser headers include Cache-Control: no-store, no-cache, must-revalidate and ETag changes on expiry And with network disabled after expiry, the previously viewed board cannot be displayed from cache
Pre-Data Enforcement and Auditing
Given any request to a TimeLock link When the server processes the request Then time-window and token validation occur before any query for Impact Board data or assets And denied requests return no board metadata, counts, or partial content And an audit entry records link ID, outcome (activated, pre-activation, expired, revoked), timestamp (UTC), and hashed token ID
One-Tap Extend/Revoke Controls
"As an organizer, I want to extend or revoke a link in one tap so that I can adapt quickly when meetings shift."
Description

Provide a single-tap control (mobile-first) to extend an active or upcoming link by common increments (+15m, +30m, +1h) or a custom time, and a one-tap immediate revoke that blocks access instantly. Extensions update the expiry in real time without changing the URL. Revoke can be undone within a short grace period, if desired, by reactivating with a new end time. Changes are reflected in the owner’s UI and recipient experience immediately.

Acceptance Criteria
Extend Active Impact Board TimeLock Link by Preset Increment
Given I am the link owner on a mobile device and the TimeLock link is currently active When I tap the +30m preset extend control Then the link’s expiry increases by exactly 30 minutes from the current expiry time And the link URL remains unchanged And the owner UI displays the new expiry within 2 seconds And recipients retain uninterrupted access with no authentication errors And the change is persisted and remains after app refresh or relaunch
Extend Upcoming TimeLock Link Prior to Activation
Given I am the link owner and the TimeLock link is scheduled but not yet active When I tap the +15m preset extend control Then the end time shifts later by exactly 15 minutes while the start time remains unchanged And the link URL remains unchanged And the scheduling details in the owner UI update within 2 seconds And recipients opening the link before the start still cannot access until the start time And recipients opening after the start can access through the new end time
Custom Extension End Time Entry and Validation
Given I am the link owner viewing an active or upcoming TimeLock link When I choose Custom and select a valid new end time Then the expiry updates to the selected time without changing the URL And the owner UI reflects the new end time within 2 seconds When I enter an end time earlier than now (for active) or earlier than the start (for upcoming) Then I see a validation message and the change is not saved And no partial update occurs and the previous expiry remains in effect
Immediate Revoke Blocks Access
Given I am the link owner and the TimeLock link is active When I tap Revoke Then access to the link is blocked within 2 seconds for both new and current sessions And recipients see an Access expired state on next interaction without needing a new URL And the owner UI shows the link as Revoked immediately And the Revoke action requires no additional confirmation prompts
Undo Revoke Within Grace Period (Reactivate with New End Time)
Given a TimeLock link was revoked within the allowed grace period When I select Reactivate and set a new valid end time Then the link becomes active immediately with the specified end time And the original URL remains unchanged And recipients regain access within 2 seconds When the grace period has elapsed Then the Undo/Reactivate option is not available
Mobile One-Tap Controls Visibility and Operability
Given I am using a mobile device (viewport width ≤ 375px) Then the +15m, +30m, +1h, Custom, and Revoke controls are visible without horizontal scrolling And each control has a minimum 44px touch target and is operable with a single tap And tapping a preset extend applies the change without opening secondary menus And tapping Revoke executes immediately and surfaces an Undo/Reactivate affordance if within grace period
Role-Gated Recipient Access
"As an organizer, I want only my board or sponsor contacts to open the link so that sensitive impact data stays limited to the right people."
Description

Restrict each TimeLock Link to designated audiences (board or sponsor) by selecting allowed recipient lists, contact tags, email domains, or named emails from GiveCrew. Enforce recipient verification via emailed magic code or one-click magic link to reduce friction while ensuring only intended people view the board. Forwarded links prompt verification; non-listed recipients are denied. Owners can add/remove recipients without regenerating the link.

Acceptance Criteria
Allowlist by List, Tag, Domain, or Named Email
Given an owner configures a TimeLock Link with allowed recipient lists, contact tags, email domains, and/or named emails in GiveCrew When a user requests access and provides an email address Then the system evaluates eligibility by matching the email against the configured lists, tags, domains, and named emails using OR logic And if a match is found, the user is marked eligible for verification And if no match is found, the request is denied before any verification email is sent with a generic access-denied message
Magic Code Verification Flow
Given an eligible user selects Verify with code When the system sends a one-time code to the user's email Then the code is 6 digits, single-use, and expires after 10 minutes And submitting the correct code within validity grants access to the Impact Board And submitting an incorrect or expired code displays an error and does not grant access
One-Click Magic Link Verification
Given an eligible user selects One-click magic link When the system emails a magic link to the user's address Then the link is single-use and expires after 15 minutes And clicking the link within validity grants access to the Impact Board And clicking the link after it is used or expired displays an error and does not grant access
Forwarded Link Handling for Non-Listed Recipients
Given a TimeLock Link URL is opened by a user who has not completed verification When the user provides an email address that is not on the allowlist Then no verification email is sent and access is denied with a generic message And when the user provides an email address on the allowlist Then a verification email is sent and access is granted only upon successful verification
Recipient Updates Without Regenerating the Link
Given an owner updates the allowlist for an existing TimeLock Link by adding or removing lists, tags, domains, or named emails When the owner saves changes Then the link URL remains unchanged And new access attempts reflect the updated allowlist immediately And users newly added can verify and gain access And users removed are denied on subsequent access attempts
Role Audience Gate (Board vs Sponsor)
Given an owner sets the audience to Board or Sponsor for a TimeLock Link When eligibility is evaluated for an email address Then only recipients associated with the selected audience via lists, tags, domains, or named emails are considered eligible And recipients associated only with the non-selected audience are not eligible unless explicitly added to the allowlist
Board View Timeframe Binding
"As an organizer, I want the link to show a fixed view and date range so that everyone discusses the same time-bounded results."
Description

Open the Impact Board with pre-set filters (campaign, region) and an optional reporting date range pinned to the meeting window, keeping conversation anchored to the intended timeframe. Lock these filters for recipients while preserving live metrics within that window. Owner can choose view-only mode for recipients to prevent edits or comments via TimeLock Links.

Acceptance Criteria
Open Board With Pinned Filters and Optional Date Window
Given an owner creates a TimeLock Link with Campaign=X, Region=Y, and a meeting Start and End time in the org’s timezone When a recipient opens the link during the active window Then the Impact Board loads with Campaign=X and Region=Y applied And the reporting date range equals Start–End in the org’s timezone And a visible TimeLock indicator displays the enforced Start–End window Given the owner omits a date range when creating the TimeLock Link When a recipient opens the link during the active window Then the Impact Board loads with Campaign=X and Region=Y applied And no additional date filter is applied beyond the default view And date filter controls are not editable for the recipient
Recipient Filter Lock Enforcement
Given a TimeLock Link with pinned Campaign, Region, and (optionally) Date Range When a recipient attempts to change Campaign, Region, or Date via UI controls Then filter controls are disabled or read-only and no change is applied And no API requests are accepted that change these filters (server-side enforcement) Given a recipient appends or modifies query parameters to alter filters When the link is reloaded Then the server ignores the tampered parameters and returns data only for the pinned filters And the UI reflects the original pinned filters within 1 second
Live Metrics Refresh Within Pinned Window
Given a TimeLock Link with a date window Start–End When a new signup, donation, or shift with a timestamp within Start–End is recorded Then the corresponding metrics on the Impact Board update within 60 seconds without widening the date window Given new data with a timestamp outside Start–End is recorded When the board auto-refreshes Then out-of-window data is not included in metrics, charts, or lists And totals match an API query using the same filters and Start–End
View-Only Mode Blocks Edits and Comments
Given the owner enables View-Only for a TimeLock Link When a recipient opens the board Then edit, annotate, and comment actions are hidden or disabled And any direct API attempt to create/update/delete content returns 403 Forbidden And no changes are persisted in the system
Activation and Expiration Access Control
Given a TimeLock Link with Start=T1 and End=T2 When a recipient accesses the link before T1 Then an access-not-yet-available screen is shown with the scheduled window and no board data is rendered When a recipient accesses the link between T1 and T2 Then the board loads successfully with pinned filters and (if set) the date window applied When a recipient accesses the link after T2 Then access is denied within 15 seconds of T2 with an expired message and no board data is rendered
One-Tap Extend or Revoke Propagation
Given a TimeLock Link is active When the owner taps Extend and selects +15 minutes Then the End time updates and all open sessions reflect the new End within 10 seconds And the header indicator shows the updated End time Given a TimeLock Link is active When the owner taps Revoke Then all current recipient sessions lose access within 10 seconds and subsequent requests return an access denied state And the link status in the owner’s list changes to Revoked
Cross-Timezone Window Consistency
Given a TimeLock Link window is defined in the owner org’s timezone When recipients in different timezones open the link Then the enforced Start–End boundaries are based on the org’s timezone And the displayed window includes the timezone label (e.g., PT, UTC−07:00) And changing a device’s timezone does not alter which records are included or excluded by the window
Access Insights & Alerts
"As an organizer, I want visibility into who accessed the link and timely alerts so that I can follow up and extend access when needed."
Description

Track and surface link activity (first/last opened, total opens, unique recipients, failed/blocked attempts) and an audit trail for create/extend/revoke events. Provide optional notifications to the owner when the link activates, when first accessed, and 10 minutes before expiry, with a one-tap extend action in the notification. Expose a lightweight report in-app and export (CSV) for compliance or sponsor follow-up.

Acceptance Criteria
Real-time Link Activity Metrics Tracking
Given a scheduled TimeLock Link with recipients managed in GiveCrew When recipients open the link during its active window Then the system records first_opened_at on the first valid access, updates last_opened_at on each subsequent valid access, increments total_opens on each valid access, and increments unique_recipients on the first valid access by each distinct recipient identifier Given a TimeLock Link When any user attempts access outside the active window or after the link is revoked or blocked by policy Then failed_or_blocked_attempts increments by 1 and first_opened_at, last_opened_at, total_opens, and unique_recipients remain unchanged Given any valid access event is recorded When the owner views the link insights in-app Then metric values reflect new events within 30 seconds and all timestamps display in the organization’s timezone (stored in UTC)
Audit Trail for Link Lifecycle Events
Given an owner creates a TimeLock Link When the link is saved Then an audit entry is written with event=create, actor=user_id, timestamp_utc, start_at_utc, end_at_utc Given an owner extends or revokes a TimeLock Link When the action is confirmed Then an audit entry is written with event=extend|revoke, actor=user_id, timestamp_utc, previous_end_at_utc, new_end_at_utc (for extend), and optional reason, and the entry is immutable Given audit entries exist for a link When the owner views the audit trail in-app Then entries are shown in reverse chronological order and can be filtered by event type (create, extend, revoke) and exported via CSV
Owner Notifications: Activation, First Access, Pre-Expiry
Given notifications are enabled for a TimeLock Link When the link’s start time is reached Then the owner receives an activation notification within 30 seconds containing the link title and scheduled window Given notifications are enabled for a TimeLock Link When the first valid access occurs within the active window Then the owner receives a first-access notification within 30 seconds containing the link title and a quick metrics summary (unique recipients, total opens) Given notifications are enabled for a TimeLock Link When the current time is 10 minutes before the scheduled expiry and the link is still active Then the owner receives a pre-expiry notification within 30 seconds Given notifications are disabled for a TimeLock Link When any of the above triggers occur Then no notifications are sent
Notification One-Tap Extend Action
Given a pre-expiry notification is delivered for an active TimeLock Link When the owner taps the Extend action Then the link’s expiry is extended by the link’s preset extend duration (default 30 minutes) without opening the app, the change is applied within 15 seconds, and the notification updates to confirm the new expiry time Given an Extend action is taken from a notification When the extension is applied Then an audit entry is recorded with event=extend, actor=user_id, channel=notification, previous_end_at_utc, new_end_at_utc Given the link is already expired at the time the Extend action is tapped When the system processes the request Then the extension is rejected with a message indicating the link has expired and provides an Open App to Reschedule action
In-App Access Insights Report
Given an owner or org admin opens the Link Insights screen When a specific TimeLock Link and date range are selected Then the report displays: first_opened_at, last_opened_at, total_opens, unique_recipients, failed_or_blocked_attempts, and a paginated audit trail (create/extend/revoke) with timestamps; loads in under 2 seconds for up to 500 events; and shows times in the organization’s timezone Given the Link Insights report is visible When the user applies filters (date range, event type) or sorts by last_opened_at or total_opens Then the report updates within 1 second and the applied filters/sorts are clearly indicated
CSV Export for Compliance and Sponsor Follow-Up
Given an owner or org admin views the Link Insights report When Export CSV is requested Then a UTF-8 CSV is generated within 30 seconds (for up to 10,000 rows) with headers: link_id, link_title, owner_id, owner_name, scheduled_start_at_utc, scheduled_end_at_utc, first_opened_at_utc, last_opened_at_utc, total_opens, unique_recipients, failed_or_blocked_attempts, event_type, event_actor, event_timestamp_utc, event_detail Given the CSV is generated When it is downloaded or delivered Then date-times are ISO 8601 in UTC, numeric fields are unquoted numbers, text is quoted as needed, and the file passes basic schema validation
Access Control and Privacy for Insights & Alerts
Given a user who is not the link owner and not an org admin When they attempt to view insights or export CSV for a TimeLock Link Then access is denied with an appropriate message and no metrics or audit data are exposed Given an org admin When they view insights or export CSV for any TimeLock Link in the organization Then access is granted and full data is visible Given an external board/sponsor viewer with a TimeLock viewing link When they attempt to access insights or alerts endpoints Then access is denied and the viewing link does not expose analytics or audit data

Watermark Shield

Overlay live, tamper-resistant watermarks with recipient name, email, timestamp, and scope on every shared view and download. Discourages unauthorized forwarding and makes leaks traceable, while remaining printer-friendly. Sponsors get clarity; your team keeps control.

Requirements

Dynamic Identity Overlay
"As an organizer sharing reports, I want on-screen views to display the recipient’s identity and context so that forwarded screenshots can be traced and discouraged."
Description

Render a live, tamper-resistant watermark across all shared views in GiveCrew (mobile and web). The overlay pulls recipient full name, email, timestamp, and access scope from the signed access token/session and tiles diagonally with randomized offsets and rotation to resist cropping. A short alphanumeric session ID is included for traceability. The overlay adapts to viewport size, orientation changes, and zoom, and respects safe zones to avoid obscuring critical content (forms, totals, names). It degrades gracefully on low-power devices and does not block interaction. Integrates with viewer components for signups, donations, shift schedules, and sponsor/Impact Board views.

Acceptance Criteria
Overlay Present on All Shared Views
Given a recipient opens the Signups viewer on web or mobile When the view finishes loading Then the watermark overlay is visible within 300 ms and remains rendered during scrolling Given a recipient opens the Donations viewer on web or mobile When the view finishes loading Then the watermark overlay is visible within 300 ms and remains rendered during scrolling Given a recipient opens the Shift Schedules viewer on web or mobile When the view finishes loading Then the watermark overlay is visible within 300 ms and remains rendered during scrolling Given a recipient opens the Sponsor/Impact Board view on web or mobile When the view finishes loading Then the watermark overlay is visible within 300 ms and remains rendered during scrolling
Identity Fields and Session ID from Signed Session
Given a valid signed access token/session with full name, email, access scope, and sessionId When any shared view renders Then the overlay text contains the exact full name, email, access scope, an ISO 8601 UTC timestamp, and the sessionId Given an attempt is made to override identity values via URL parameters, local storage, or DOM injection When the view renders Then the overlay values remain unchanged and match only the signed token/session claims Given a valid sessionId When the overlay renders Then the sessionId displayed is 8–12 alphanumeric characters and remains constant for the session duration
Tamper-Resistant Diagonal Tiling
Given any viewport size between 320x568 and 2560x1440 When the overlay renders Then watermarks are tiled diagonally at a rotation between 20° and 35° Given the overlay renders When comparing two consecutive render events (e.g., reload) Then x/y tile offsets differ by at least 16 px and rotation differs by at least 5° Given any on-screen 300x300 px area When inspected Then at least one watermark tile is visible within that area Given a user crops or screenshots removing up to 15% from any single edge When the image is reviewed Then at least one watermark tile with identity fields remains visible
Responsive to Viewport, Orientation, and Zoom
Given a device rotates between portrait and landscape When the orientation change completes Then the overlay reflows within 200 ms and maintains diagonal tiling and minimum tile density (≥1 tile per 300x300 px) Given a desktop browser zoom between 50% and 200% When the page is zoomed Then overlay text scales to remain legible (font size 10–18 px) and tile density varies by no more than ±20% Given a window is resized by ≥25% in width or height When the resize ends Then the overlay recalculates positions within 200 ms without flicker or blank frames
Safe Zones and Non-Blocking Interaction
Given critical UI elements (form inputs, totals, entity names, primary action buttons) are marked as safe zones When the overlay renders Then no more than 5% of any safe-zone element’s bounding box is visually covered by watermark glyphs Given a user taps, clicks, scrolls, drags, or types anywhere in a shared view When interacting Then all events pass through; the overlay consumes no pointer or keyboard events (pointer-events disabled; overlay is aria-hidden and unfocusable) Given keyboard navigation or screen reader usage When traversing the view Then focus order and announcements are unchanged by the overlay
Printouts and Screenshots Preserve Traceability
Given a user prints or saves to PDF any shared view When the print output is generated Then the overlay appears on every page with full name, email, access scope, ISO 8601 UTC timestamp, and sessionId at 6–12% opacity and remains legible at 300 DPI Given a user captures a screenshot on iOS, Android, or desktop OS When the image is reviewed Then the overlay is present in the screenshot with all identity fields and the sessionId Given a session persists across multiple pages When printing or capturing each page Then the same sessionId value appears consistently on those pages
Performance and Graceful Degradation
Given a mid-tier device (e.g., 4 CPU cores, 4 GB RAM) When a shared view loads Then overlay initialization adds ≤50 ms to time-to-interactive and CPU overhead averages <5% during idle viewing Given frame time exceeds 16 ms for 3 consecutive frames or the device is in low-power mode When the condition is detected Then the overlay reduces tile density by 30–50% and uses static rendering (no animations) within 300 ms while remaining visible Given a 60-second continuous scroll and interaction session When monitored Then no crashes or unhandled errors occur and memory usage attributable to the overlay remains <10 MB
Protected Export Watermarking
"As a program lead exporting rosters and receipts, I want downloads to carry non-removable identity marks so that if files leak we can attribute and reduce misuse."
Description

Embed recipient identity, email, timestamp, scope, and a unique export ID directly into downloaded artifacts. For PDFs and image exports (e.g., Impact Board posters), flatten the watermark into the content layer on every page to prevent removal, with perimeter edge marks and tiled diagonal text to resist cropping. For spreadsheets/CSV, prepend a frozen header row and footer note with identity and export ID, and write matching identity/export metadata into file properties and the audit log. Support batch exports, multi-page documents, and locale-aware timestamps. Ensure watermarks remain legible yet unobtrusive in print and digital contexts.

Acceptance Criteria
PDF/Image Exports: Flattened, Page-Wide, Tamper-Resistant Watermarks
- Given a user exports a multi-page PDF or image artifact, When the file is opened in a layer-aware editor, Then watermark elements are part of the content layer and cannot be independently selected or removed. - Given the exported file, When a page is cropped to remove center content, Then perimeter edge marks remain visible on all sides where content remains. - Given the exported file, When any single crop retains ≥70% of original page area, Then at least one full instance of tiled diagonal watermark text containing the export ID remains visible. - Given the exported file, When printed on A4/Letter at 100% scale on a standard laser printer, Then watermark text is legible (≥9pt equivalent) and does not obscure more than 5% of primary content area.
Identity, Scope, Timestamp, and Export ID Accuracy
- Given an authenticated recipient with name, email, and a selected share scope, When they initiate an export, Then the flattened watermark text includes the recipient name, recipient email, the current timestamp formatted per recipient locale with timezone offset, the scope label, and a unique export ID. - Given two exports created in the same second for the same recipient and scope, When inspecting the embedded watermarks, Then the export IDs are distinct. - Given the audit event for the export, When cross-checking against the watermark contents, Then all fields exactly match. - Given a recipient whose locale is de-DE and timezone Europe/Berlin, When exporting, Then the timestamp appears in de-DE format with CET/CEST offset.
Spreadsheet (XLSX) Export Watermarking and Metadata
- Given an XLSX export, When opening the workbook, Then the first row is a frozen header that includes recipient name, email, scope, timestamp (locale-aware), and export ID. - Given the XLSX export, When scrolling, Then the header row remains visible (frozen) across all sheets. - Given the XLSX export, When inspecting core file properties, Then custom properties exist for RecipientName, RecipientEmail, Scope, ExportTimestamp, and ExportId with values matching the header. - Given the XLSX export event, When viewing the audit log, Then an entry with the same export ID is present.
CSV Export Watermarking and Metadata
- Given a CSV export, When opening the file, Then the first line is a header row containing recipient name, email, scope, timestamp (ISO 8601 with timezone), and export ID. - Given the CSV export, When reading the last line, Then a footer note is present that repeats the export ID and recipient email. - Given the CSV export, When inspecting file properties on platforms that support extended file properties, Then metadata fields for RecipientName, RecipientEmail, Scope, ExportTimestamp, and ExportId are present and match the header. - Given the CSV export event, When viewing the audit log, Then an entry with the same export ID is present.
Batch Export Consistency and Uniqueness
- Given a batch export request for N artifacts for the same recipient, When the export completes, Then each artifact contains the correct recipient name, email, scope, locale-aware timestamp, and a unique export ID per artifact. - Given the batch export, When viewing the audit log, Then there are N entries, each with its artifact type, page count (if applicable), and export ID, and a common batch/job ID linking them. - Given the batch export, When any single artifact is opened, Then its watermark fields match its corresponding audit entry.
Multi-Page Documents Coverage
- Given a multi-page PDF export with P pages, When inspecting pages 1..P, Then each page contains edge marks on all four sides and tiled diagonal watermark text. - Given the multi-page PDF, When extracting any single page, Then that page alone still contains the identity fields and export ID. - Given the multi-page PDF, When merging with other PDFs, Then the watermark remains visible and unaltered on the merged pages.
Usability—Legible Yet Unobtrusive
- Given a standard digital view at 100% zoom and standard print at 300 DPI, When reviewing the artifact, Then recipient name, email, and export ID are readable without zooming and do not occlude more than 5% of non-white content area. - Given images with dark or light backgrounds, When rendering the watermark, Then dynamic contrast adjustment ensures a minimum contrast ratio of 3:1 for watermark text against its local background. - Given a grayscale print, When reviewing the artifact, Then the watermark remains readable.
Shared Link Recipient Binding
"As a staffer sending a sponsor progress view, I want the link tied to that sponsor’s name and email so that any forwarded access is still traceable to the original recipient."
Description

Bind each shared view/download link to a specific recipient via one-time, signed tokens that carry identity and scope. Options include enforced email verification, SSO passthrough, expiration windows, and revocation. The watermark renders using the bound identity even if a link is forwarded, ensuring traceability. Provide SDK hooks for existing share flows (sponsor updates, volunteer rosters, donor receipts) and support deep links from email/SMS. Display clear inline labels when the link is expired or revoked.

Acceptance Criteria
Enforced Email Verification Before Access
Given a shared link configured with Enforced Email Verification and bound to recipient alice@example.org When the recipient opens the link Then the page prompts for email and sends a one-time code to alice@example.org And when the correct code is submitted within 10 minutes and no more than 5 attempts Then access is granted and the view renders with a watermark showing the bound name, email, timestamp, and scope And on mismatch/timeout/exhausted attempts, access is denied with an inline error and no content rendered And an audit log entry is recorded for each attempt including outcome, IP, and user agent
Forwarded Link Traceability Without Enforcement
Given a shared link bound to Alice and created without email verification enforcement When any person opens the link (including via forwarding) Then the content renders and the watermark displays Alice’s bound identity (name, email) and current timestamp and scope And the page shows a non-blocking inline notice “This link is personalized for Alice” with a Manage Access link And the bound identity cannot be altered client-side (attempts to change query parameters or headers do not change the watermark) And an audit log records the viewer’s IP and user agent while attributing the session to Alice’s bound identity
Expiration Window Enforcement and Messaging
Given a shared link with expiration set to 2025-08-31T23:59:59Z (server time authoritative, ±2 minutes drift tolerated) When the link is opened before expiration Then access is allowed and the watermark renders with the current timestamp When the link is opened after expiration or the token TTL elapses Then the content is not rendered and an inline label “Link expired” with a Re-request Access CTA is displayed And all API calls return HTTP 410 Gone with machine-readable code link_expired And the SDK getStatus() returns {state:"expired"} for the token
Revocation Propagation and Session Invalidation
Given a link is active and then revoked via the admin dashboard or API When any user opens the link after revocation Then the content is not rendered and an inline label “Access revoked” is shown And active sessions viewing the content receive a revocation event and are blocked within 60 seconds, replacing the view with the same label And API responses return HTTP 403 with code link_revoked And audit logs capture the revocation actor, time, and affected token
SSO Passthrough Binding
Given a link configured for SSO passthrough (OIDC/SAML) and bound to recipient with external_id 123 and email carol@org.org When Carol is signed-in via the configured IdP and opens the link Then access is granted without extra email verification and the watermark renders with Carol’s bound identity and scope And when a different signed-in user opens the link, access is denied or re-auth prompted per policy and no content is rendered And IdP assertions are validated (audience, signature, expiry) and mismatches are logged And the SDK getStatus() reports state:"active", auth:"sso" on success
Deep Link Preservation from Email/SMS
Given an email or SMS deep link containing the signed token and route parameters When the link is opened on iOS, Android, or desktop Then the SDK preserves the bound identity and scope, routes to the target resource, and renders the watermark accordingly And if the native app is installed, the link opens in-app; otherwise it falls back to the web view without losing the token And the token is never leaked via referrer headers or third-party redirects (validated against at least two common email clients and two mobile browsers) And malformed or tampered deep links are rejected with inline “Invalid link” messaging and HTTP 400 code link_invalid
SDK Hooks for Share Flows Integration
Given a developer integrates the SDK in a web or mobile app for sponsor updates, volunteer rosters, and donor receipts When they call createBoundLink(identity:{name,email}, scope, options) on the server and renderWatermark(container) on the client Then the generated URLs resolve to bound views, the watermark displays the provided identity and scope, and downloads are tagged with the bound identity And calling revokeLink(token) causes getStatus(token) to return state:"revoked" and existing sessions to invalidate within 60 seconds And sample implementations for sponsor updates (PDF), volunteer rosters (CSV), and donor receipts (HTML) pass the provided integration tests And SDK methods are available for Web (JavaScript), iOS (Swift), and Android (Kotlin) with consistent API signatures and versioned documentation
Printer-Friendly Rendering Mode
"As a volunteer coordinator who needs to print rosters, I want watermarks that don’t obscure content so that printed pages remain usable while still traceable."
Description

Automatically adjust watermark styling for print to remain readable without obscuring content. On print or PDF generation, switch to calibrated grayscale/monochrome, reduce opacity, increase letter spacing, and place non-overlapping bands with safe margins around key data fields. Provide admin presets (Subtle, Standard, Strong) and per-template overrides for rosters, receipts, and Impact Board posters. Validate output across common desktop and office printers and ensure consistent results from mobile AirPrint/Cloud Print.

Acceptance Criteria
Auto Switch to Printer-Friendly on Print/PDF
Given a user views a roster, receipt, or Impact Board poster with Watermark Shield enabled And initiates OS/System print or Export/Save to PDF (including system "Print to PDF") When the print job is prepared Then mode is set to printer_friendly for the output pipeline And watermark colors are converted to grayscale/monochrome (K-only), with CMY channels each ≤ 2% And preset parameters for opacity, letter spacing, and band pitch are applied to the watermark And the printer-friendly styling is embedded into the printed/PDF output only And on-screen (non-print) watermark styling remains unchanged
Non-Obscuring Placement with Safe Margins
Given a template defines key data fields with bounding boxes (e.g., names, totals, shift times) When the printer-friendly watermark is laid out Then no watermark band intersects any key data field bounding box (0 intersections) And a minimum 6 mm safe margin is maintained around each key data field boundary And a minimum 8 mm edge margin is maintained from all page edges And watermark bands do not overlap each other And underlying document text remains fully selectable and copyable in generated PDFs
Admin Presets and Per-Template Overrides
Given an org admin opens Watermark Shield settings When they select a preset (Subtle, Standard, Strong) and save Then the following parameters are stored and applied by default unless overridden: - Subtle: opacity 6–8% K; letter spacing +5–8%; band pitch 80–100 mm - Standard: opacity 9–12% K; letter spacing +10–12%; band pitch 60–80 mm - Strong: opacity 13–16% K; letter spacing +14–16%; band pitch 40–60 mm And per-template overrides can be configured for Rosters, Receipts, and Impact Board posters And per-template settings take precedence over the org default And a live preview reflects the effective parameters before saving And saved settings persist and are applied in subsequent print/PDF jobs
Cross-Device and Printer Consistency
Given the same source document and selected preset When printing via a representative matrix: - Windows 11 → HP LaserJet (mono) - macOS 14 → Canon office MFP (color) - macOS "Save as PDF" - iOS 17 AirPrint → Brother laser - Android 14 system print → Epson inkjet Then the watermark renders in grayscale/monochrome only (CMY ≤ 2%) on all outputs And band angle, pitch, and safe margins match across outputs within ±1.5 mm And recipient name, email, timestamp, and scope are present and readable on all outputs
Watermark Readability and Content Completeness
Given printer-friendly mode is active When a page is printed at 300–600 DPI on A4 or Letter Then the watermark includes recipient full name, email, timestamp (UTC ISO-8601), and scope And minimum text height is ≥ 2.8 mm for Subtle and ≥ 3.2 mm for Standard/Strong And effective grayscale density of watermark text is within: - Subtle: 12–18% K - Standard: 18–25% K - Strong: 25–32% K And increased letter spacing is applied per preset to maintain legibility over light/dark backgrounds
Multi-Page, Orientation, and Scaling Support
Given documents of 1–50 pages in portrait or landscape and sizes A4, Letter, Tabloid, and poster templates When printing with scaling options: 100%, Fit to page, 90% Then watermark band angle and pitch are preserved in physical units (mm), unaffected by scaling And safe margins around key fields are preserved post-scaling And no watermark bands are clipped at page edges And all pages in a job share the same job timestamp value rendered in the watermark
Performance and Fail-Safe Behavior
Given any supported document up to 10 pages When initiating print/PDF generation with printer-friendly mode Then watermark processing adds ≤ 300 ms per page on a mid-tier device And peak additional memory usage is ≤ 50 MB per 10 pages And if key fields are not tagged, a default 10 mm page-edge safe margin is applied And if destination capabilities are unknown, fallback uses Standard preset in grayscale And on renderer failure, printing proceeds without watermark modification and a non-blocking error is logged
Audit Trail and Leak Attribution
"As a compliance officer, I want a searchable audit trail that links any watermark ID to a person and event so that we can respond quickly to suspected leaks."
Description

Record immutable events for each view and export, including user/recipient identity, scope, timestamp, IP/device fingerprint, and the session/export ID embedded in the watermark. Expose an admin search that resolves a visible watermark ID (or QR/microtext code) to the corresponding event with downloadable evidence (who, what, when, which artifact). Provide webhook events for SIEM integration and retention policies aligned with nonprofit compliance needs. Support rapid lookups during incident response and show linkage across forwarded views of the same token.

Acceptance Criteria
Event Logging on View and Export
Given a recipient opens a watermarked share link within scope X When the view is rendered Then an immutable event is stored with non-null fields: event_type=view, scope=X, token_id, session_id, user_id_or_recipient_email, timestamp_utc (ISO 8601), ip, device_fingerprint, user_agent, artifact_id Given a file export/download is initiated When the export completes Then an immutable event is stored with event_type=export and includes export_id and token_id embedded in the watermark Given an admin attempts to modify any stored event via UI or API When the request is made Then the operation is rejected with 403/405 and the attempt is logged as a security event Given an event is written When queried by token_id within 30 days Then the event is available for search within 5 seconds p95
Admin Search by Watermark ID/Code
Given an admin enters a visible watermark ID, QR, or microtext code in Audit Search When the query is submitted Then matching event(s) are returned within 2 seconds p95 for last-30-day data and 5 seconds p95 for older data Given results are returned When inspecting a result Then the record shows who (user/recipient identifier), what (event_type), when (timestamp_utc), which_artifact (artifact_id), ip, device_fingerprint, and session_or_export_id Given no match exists When the query is submitted Then the system returns "No results" within the same SLA without exposing any PII Given results are returned When the admin selects "Download Evidence" Then a ZIP download begins containing the evidence bundle for the selected event
Tamper-Evident Evidence Bundle
Given an admin downloads an evidence bundle for an event When validating the manifest Then SHA-256 hashes of all files match and the detached signature verifies against the platform public key Given the event JSON contains hash_chain_prev and hash_chain_curr When submitted to the verification endpoint/CLI Then the verification returns valid=true for unmodified records and valid=false if any field has been altered Given the bundle includes a signed server timestamp When validating within a clock skew of ±5 minutes Then the timestamp is accepted; otherwise the verification report flags a time integrity warning
Webhook Delivery for SIEM
Given webhooks are configured with endpoint URL and shared secret When a view or export event is recorded Then a POST is delivered within 10 seconds p95 with headers X-Signature (HMAC-SHA256), X-Event-Type, and X-Delivery-Id, and a JSON body including token_id, event_type, timestamp_utc, artifact_id, session_or_export_id, ip, device_fingerprint, retention_end_at, legal_hold Given the endpoint responds with non-2xx or times out When delivery is attempted Then retries occur with exponential backoff up to 10 attempts over 24 hours; success (2xx) stops retries; failures are logged and visible in Delivery Logs Given webhook filters are set to event_type=view and PII redaction=on When an export event occurs Then no webhook is sent Given PII redaction=on When a view event is delivered Then ip and device_fingerprint fields are redacted in the payload Given an admin opens Delivery Logs When filtering by delivery_id Then the complete attempt history and status are displayed
Retention and Legal Hold Compliance
Given an org configures retention windows (e.g., views=365 days, exports=2555 days) and no legal hold When events reach retention_end_at Then they are purged within 24 hours and are no longer returned in search, exports, or webhooks Given a legal hold is applied to token_id T When daily retention processing runs Then all events linked to T are retained and surfaced with legal_hold=true until the hold is removed Given a legal hold is removed When the next retention cycle runs Then any events past retention are purged and a hold-change record is written to the audit log Given an API or UI request targets a purged event When the event id is queried Then a 404 Not Found is returned with reason=retention_expired
Incident Response Token Correlation and Forwarding Linkage
Given multiple recipients view a shared link that was forwarded and retains the same token_id When an admin opens the Incident Correlation view for that token Then a timeline and correlation graph display all linked events with first_seen, last_seen, total_events, unique_ips, and approximate geolocation summaries Given the token has up to 10,000 events in the last 7 days When loading the correlation view Then p95 render time is under 3 seconds Given the correlation view is open When the admin exports correlated events Then CSV and JSON files are downloaded containing all events, with evidence_bundle_url per event Given an admin searches by recipient_email or ip When matching events exist Then results include the associated token_id and a link to open the correlation view
Admin Controls and Policies
"As an org admin, I want to configure watermark behavior per audience so that sponsors see clear labels while internal teams keep a less intrusive mark."
Description

Offer tenant-level and role-based settings to configure Watermark Shield: enable by content type (views, PDFs, image exports, CSV), choose displayed fields (name, email, timestamp, scope), set opacity/tiling density, require recipient binding/verification, define token expiry/reuse rules, and restrict overrides to admins. Provide a live preview with sample data, policy presets for common scenarios (Internal, Sponsor, Public Preview), and audit of configuration changes. Defaults favor security while preserving usability.

Acceptance Criteria
Tenant-Level Enablement by Content Type
Given a new tenant, When default settings are loaded, Then Watermark Shield is enabled by default for views, PDFs, image exports, and CSV exports. Given an admin toggles a content type off and saves, When content of that type is shared or exported, Then no watermark is applied and the action is logged with the active policy and content type. Given an admin toggles a content type on and saves, When content of that type is shared or exported, Then the watermark renders on every generated output of that type. Given changes are saved, When any admin reopens the settings page, Then the saved content-type selections persist tenant-wide. Given a non-admin attempts to modify content-type toggles, When they try to save, Then the action is denied with a permissions error and no changes are saved. Given access from mobile and web surfaces, When content is generated, Then content-type enforcement is consistent across all surfaces and APIs.
Watermark Appearance Configuration
Given default policy values, When settings are first opened, Then displayed fields are name, email, timestamp, and scope; opacity is 35%; tiling density is Normal. Given an admin selects or deselects displayed fields and saves, When a watermark is rendered, Then the overlay includes exactly the selected fields in the configured order. Given an admin attempts to save with zero displayed fields selected, When saving, Then the save is blocked with a validation message requiring at least one field (name or email must be among the selected fields) and no changes persist. Given an admin sets opacity to a value outside 15%–60%, When saving, Then the save is blocked with a validation message and no changes persist. Given an admin adjusts tiling density (Sparse/Normal/Dense) or spacing within 150–400px equivalent, When saving, Then subsequent renders and the live preview reflect the new density/spacing. Given any appearance change, When viewing the live preview, Then the preview updates within 300ms and uses sample data (not live PII).
Recipient Binding and Verification Enforcement
Given policy requires recipient binding, When a share is created, Then each link is bound to specified recipient identity (name/email) and the bound identity is included in the watermark overlay. Given policy requires verification, When a bound recipient accesses content from a new browser or device, Then they must complete email verification via signed link or OTP before any content is rendered. Given a recipient has not completed verification, When they attempt to access the content, Then access is denied, no preview is shown, and the event is logged. Given a verified recipient accesses content, When the watermark renders, Then the overlay displays the verified recipient’s name/email and access is logged with recipient identity and share ID. Given an unbound identity attempts to use a bound link, When access is attempted, Then access is denied and the event is logged.
Token Expiry and Reuse Rules
Given an admin sets token expiry duration and max uses, When a recipient attempts access after expiry or after exceeding max uses, Then access is denied with an expiration message and the event is logged. Given single-use tokens are enabled, When the token is used once, Then any subsequent attempt with the same token is denied. Given an admin revokes or rotates a token, When a recipient attempts access with the revoked token, Then access is denied within 60 seconds across web and mobile surfaces. Given minor client clock skew, When evaluating token expiry, Then the system bases decisions on server time and does not falsely deny valid tokens. Given a token is valid and within use/expiry limits, When accessed by the bound, verified recipient (if required), Then access is granted and the watermark renders.
Role-Based Override Restrictions
Given default roles (Admin, Manager, Staff, Volunteer), When a non-admin opens Watermark Shield settings, Then all controls are read-only and per-item override options in share dialogs are hidden or disabled. Given an admin opens a share dialog, When attempting to disable or alter the watermark for that specific share, Then the action requires explicit confirmation and reason entry, and is allowed; the override is recorded. Given a non-admin attempts the same override, When they confirm, Then the action is denied with a permissions error and no override is applied. Given role changes are updated by an admin, When the affected user refreshes their session, Then the new permissions take effect immediately for settings and share dialogs. Given any denied override attempt, When it occurs, Then an audit event is captured with actor, role, timestamp, target, and reason (if provided).
Policy Presets: Internal, Sponsor, Public Preview
Given the Internal preset is applied, Then watermark is enabled for all content types; displayed fields include name, email, timestamp, and scope; recipient binding and verification are required; opacity is 35% with Normal density; token expiry defaults to 7 days; non-admin overrides are disabled. Given the Sponsor preset is applied, Then watermark is enabled for all content types; displayed fields include name, email, timestamp, and scope; recipient binding and verification are required; opacity is 30% with Dense density; token expiry defaults to 72 hours; non-admin overrides are disabled. Given the Public Preview preset is applied, Then watermark is enabled for views, PDFs, and image exports (and for CSV if enabled by tenant policy); displayed fields include timestamp and scope only; recipient binding and verification are not required; opacity is 25% with Sparse density; token expiry defaults to 24 hours. Given any preset is applied, When the admin modifies any individual setting, Then the preset label changes to Custom and the modified values persist. Given Custom is active, When Reset to Preset is selected for a chosen preset, Then all settings revert to that preset and the live preview updates immediately.
Configuration Change Audit Trail
Given any Watermark Shield setting or preset is changed, When the change is saved, Then an immutable audit entry records actor, role, tenant, UTC timestamp, attribute, old value, new value, and source (UI or API). Given the audit log is viewed, When filtering by actor, date range, attribute, or preset, Then results update within 1 second for up to 10,000 matching entries. Given an admin exports the audit log, When Export CSV is requested, Then a CSV is generated with applied filters and includes an integrity checksum (e.g., SHA-256) of the export content. Given a non-admin attempts to edit or delete an audit entry, When the action is requested, Then it is denied; there is no edit/delete capability for audit entries. Given default retention, When entries exceed 365 days, Then they remain queryable via archived storage and continue to appear in filtered results with the same fields.
Performance and Compatibility
"As a mobile user on spotty networks, I want watermarks to appear instantly without lag so that sharing and printing remain smooth in the field."
Description

Guarantee fast, reliable watermarking across devices and formats. Client overlays should initialize in under 50 ms and animate within 16 ms per frame on scroll/zoom. Export pipeline should add watermarks with minimal added latency (<500 ms for single-page, scalable for multi-page). Ensure compatibility with iOS/Android apps, in-app WebViews, modern browsers, and common PDF viewers; provide fallbacks where advanced features aren’t supported. Instrument performance metrics, add feature flags for gradual rollout, and implement graceful degradation on low-memory devices.

Acceptance Criteria
Mobile WebView Initialization Performance
Given a supported device (iOS 15+ or Android 9+) using an in-app WebView When a recipient opens any Watermark Shield–protected shared view Then the client watermark overlay initializes and becomes visible within 50 ms at the 95th percentile And no initialization errors are logged (error rate < 0.1%)
Scroll/Zoom Animation Frame Budget
Given a supported device viewing a Watermark Shield–protected page When the user scrolls continuously for 10 seconds or performs pinch-zoom interactions Then watermark overlay updates render within 16 ms per frame at the 95th percentile And dropped frames do not exceed 5% during the interaction
Single-Page PDF Export Latency
Given a single-page document export request with Watermark Shield applied When the export pipeline processes the request Then watermarking adds less than 500 ms end-to-end latency at the 95th percentile And the exported PDF displays the watermark correctly in Acrobat, Apple Preview, and Chrome PDF viewer
Multi-Page Export Scalability Under Load
Given 10, 50, and 100-page documents and 10 concurrent export requests When the export pipeline runs with Watermark Shield enabled Then added latency scales at no more than 120 ms per additional page at the 95th percentile And the export success rate is at least 99.5% with no timeouts
Cross-Platform Runtime Compatibility
Given modern platforms (iOS Safari 15+ / WKWebView, Android Chrome 100+ / Android System WebView 100+, and desktop Chrome/Edge/Firefox/Safari latest and latest-1) When a user views Watermark Shield–protected content Then the watermark (name, email, timestamp, scope) renders correctly, remains visible on scroll/zoom, and does not break layout or controls And if required APIs are unavailable (e.g., CSS blend modes, OffscreenCanvas), a static tiled fallback watermark is applied without script errors
Graceful Degradation on Low-Memory Devices
Given low-memory conditions (Android memory class ≤ 3 or iOS Low Power Mode with < 1 GB free) When viewing and scrolling Watermark Shield–protected content Then the client overlay degrades to non-animated or lower-resolution mode or switches to server-rendered watermarking without crashing And in degraded mode, average frame time remains ≤ 25 ms and additional memory usage from the feature is ≤ 10 MB
Instrumentation and Feature-Flagged Rollout
Given telemetry and feature flags are enabled for Watermark Shield When the feature is rolled out to X% of sessions per platform Then metrics are captured for init_time_ms, frame_time_ms, export_latency_ms, memory_usage_mb, viewer_type, and device_class with ≥ 95% event coverage And the feature can be enabled/disabled per platform via flag with effect within 5 minutes, and a kill switch disables client overlay while preserving server-side watermarking

Poster Scopes

Pick an audience preset—Board, Sponsor, or Public—to instantly scope which metrics appear, how they’re grouped, and what’s hidden. Save custom scopes for repeat use so Campaign Architects can ship safe sharelinks in seconds. Reduces errors and ensures no PII slips through.

Requirements

Audience Preset Selector
"As a Campaign Architect, I want to pick an audience preset so that I can scope a poster in seconds without configuration errors."
Description

Provide a mobile-first selector with preset options (Board, Sponsor, Public) that instantly applies predefined scope configurations to the Impact Board. Selecting a preset loads its associated metric inclusions/exclusions, groupings, labels, and privacy filters, with safe defaults if a configuration is incomplete. The selector supports per-campaign defaults, remembers the last-used choice per user, and validates data availability before applying. It integrates with the poster rendering pipeline and sharelink generation so the chosen scope remains locked through preview, export, and sharing. The UI is accessible, responsive, and resilient to network latency with optimistic updates and rollback on failure.

Acceptance Criteria
Instant Apply of Audience Preset to Impact Board
Given a campaign with Board, Sponsor, and Public presets configured And the Impact Board is loaded on a mobile device When the user selects the "Sponsor" preset from the Audience Preset Selector Then the board re-renders using the Sponsor scope's metric inclusions, exclusions, groupings, labels, and privacy filters And PII fields (name, email, phone, address, notes) are excluded per the Sponsor scope configuration And an optimistic visual update is shown within 150 ms of selection And the final state matches server-confirmed configuration within 2 s or rolls back if the server rejects the change
Safe Defaults When Preset Configuration Is Incomplete
Given a preset is missing any of the following: groupings, labels, or privacy filters for included metrics When the user applies that preset Then the system fills missing values using the documented safe defaults And privacy filters default to the most restrictive setting (no PII) when unspecified And no undefined, placeholder, or raw keys appear in the UI And the board remains renderable without errors And a non-blocking notice indicates defaults were used
Per-Campaign Default and Last-Used Memory
Given Campaign A defines Public as its default preset And the user has never chosen a preset for Campaign A When the user opens the Impact Board for Campaign A Then Public is preselected and applied Given the same user later selects Board for Campaign A When the user returns to Campaign A's Impact Board on the same account Then Board is preselected and applied And this memory is scoped per user per campaign and does not affect Campaign B
Data Availability Validation Before Apply
Given a preset references metrics that require data not currently available (e.g., API outage or missing dataset) When the user selects that preset Then the system validates data availability before applying And any unavailable metrics are omitted using safe defaults and visually marked as unavailable And a non-blocking banner lists the omitted metrics with a Retry action And no partial or stale data is displayed for those metrics
Scope Lock Through Preview, Export, and Sharelink
Given the user has applied the Public preset When the user opens Preview, exports a poster (PDF/PNG), or generates a sharelink Then all outputs reflect exactly the Public scope as currently applied And the generated sharelink encodes the scope identifier and is immutable with respect to scope after creation And opening the sharelink on another device shows the same scoped poster without exposing any PII And changing the preset in the UI after generating a sharelink does not change previously created sharelinks And the server enforces the scope from the sharelink and rejects attempts to alter scope via client parameters
Accessible, Mobile-First Audience Preset Selector
Given a device width between 320 px and 768 px When navigating the Audience Preset Selector via touch, keyboard, and screen reader Then each option has an accessible name, role, and state (selected/unselected) announced by assistive tech And touch targets are at least 44x44 px with a visible focus indicator And color contrast for text and controls is at least 4.5:1 And the selector is operable with a keyboard without traps and supports screen readers (e.g., VoiceOver/TalkBack) And orientation changes do not cause loss of selection or content overlap
Optimistic Update With Rollback on Failure
Given network latency up to 1000 ms or a transient server error occurs When the user selects a preset Then the UI shows an optimistic selection state within 150 ms And if the server confirms within 5 s, the optimistic state persists with no further visual shift beyond data reconciliation And if the server rejects the change, the board reverts to the previous scope within 200 ms and a clear error message is shown with a Retry action And no mixed-state UI (e.g., header shows Sponsor while body shows Board) is visible for more than 100 ms
Metric Visibility Rules Engine
"As a Campaign Architect, I want the system to enforce metric visibility rules so that the poster always reflects the correct data grouping and naming for each audience."
Description

Implement a configuration-driven rules engine that maps audience scopes to metric visibility, grouping, aggregation windows, labels, and formatting. Rules support hierarchical precedence (preset baseline → organization defaults → campaign overrides → per-scope customizations) and allow computed metrics with safe, auditable transformations. The engine enforces field-level inclusion, aliasing, and ordering, and outputs a normalized configuration consumed by the poster renderer and sharelink service. Configurations are schema-validated, versioned, and multi-tenant aware, with caching for performance and a test harness to prevent regressions when new metrics are introduced.

Acceptance Criteria
Resolve Hierarchical Precedence Across Scope Layers
Given preset baseline sets DonationsTotal.visibility=false And org defaults set DonationsTotal.visibility=true And campaign overrides set DonationsTotal.label="Donations" And per-scope customizations set DonationsTotal.visibility=false and DonationsTotal.label="Gifts" When the engine resolves for tenant T, campaign C, scope S Then DonationsTotal.visibility=false and DonationsTotal.label="Gifts" Given aggregationWindow is unset at per-scope and set to "rolling_28d" at campaign override And org default is "calendar_month" and baseline is "all_time" When resolved Then aggregationWindow="rolling_28d" Given grouping differs between org defaults and campaign overrides When resolved Then campaign overrides take precedence over org defaults Given a property is absent at a higher-precedence layer When resolved Then the next lower non-null value is selected Given a property is explicitly null at a higher-precedence layer When resolved Then lower-layer values for that property are ignored (null wins)
Field-Level Inclusion, Aliasing, Ordering, and Normalized Output
Given scope config includes metrics [A,B,C] with aliases ["Signups","Donors","Hours"] and order [B,C,A] When normalized Then output metrics are ordered [B,C,A] with labels ["Donors","Hours","Signups"] Given metric D is marked exclude or is PII and not whitelisted When normalized Then D does not appear in output Given duplicate metric ids exist in input When normalized Then output contains a single instance using the highest-precedence definition Given unknown metric id "X" is referenced When validated Then validation fails with METRIC_UNKNOWN and no normalized output is produced Given formatting spec for A is currency:USD and sample value 1234 When formatted Then output renders "$1,234.00" Given subfield donor_email has include=false When normalized Then donor_email is absent from raw and computed outputs
Computed Metrics with Safe, Auditable Transformations
Given computed metric ShowUpRate = attended_shifts / scheduled_shifts with inputs attended=44 and scheduled=50 When evaluated Then value=0.88 and formatted value="88%" Given denominator is 0 or any required input is missing When evaluated Then value="N/A" and no exception is thrown Given a transformation references PII field donor_email When evaluated Then evaluation is blocked with METRIC_PII_VIOLATION Given any computed metric is evaluated When evaluated Then an audit record is persisted with {formulaId, formulaVersion, inputMetricIds, inputValues (non-PII), scopeId, timestamp} Given formula show_up_rate updates from v1 to v2 When resolving a scope pinned to v1 Then v1 is used and results remain reproducible Given allowed operations are {+, -, *, /, min, max, sum, count, if, else, round} When any other function is used Then evaluation fails with METRIC_FUNC_NOT_ALLOWED
Schema Validation and Versioning of Scope Configurations
Given a scope config JSON When validated Then it conforms to schema v{major.minor} with required fields {scopeId, tenantId, version, metrics[]} Given a required field is missing When validated Then response is 422 with SCHEMA_REQUIRED and a JSON path pointer to the missing field Given an unknown additional top-level property exists When validated Then response is 422 with SCHEMA_ADDITIONAL_PROP and a path to the offending property Given config version 1.3 is submitted and engine supports 1.x When validated Then it is accepted Given config version 2.0 is submitted and engine supports 1.x When validated Then it is rejected with VERSION_INCOMPATIBLE Given a minor schema bump from 1.3 to 1.4 When existing stored configs are revalidated Then they pass without modification
Multi-Tenant Isolation in Rules Resolution
Given tenants A and B have different org defaults for VolunteerHours When resolving for tenant A Then only tenant A defaults are applied Given caches are populated for tenant A When resolving for tenant B Then cache keys are tenant-scoped and no values leak across tenants Given a sharelink generated under tenant A When accessed with tenant B context or unauthenticated Then response is 403 and no configuration is returned Given a user with role Campaign Architect in tenant A When attempting to override tenant B config Then access is denied with AUTH_FORBIDDEN
Caching Performance and Invalidation for Scope Resolution
Given repeated resolution of the same scope under steady load of 200 RPS per region When measured Then p95 resolution time <= 75 ms and p99 <= 150 ms Given org defaults, campaign overrides, or metric registry change When published Then affected cache entries invalidate within 5 seconds and subsequent resolutions reflect the change Given a scope is published When cached Then TTL is 10 minutes with sliding refresh and no entry is served stale beyond TTL Given cache is cold When first resolution occurs Then latency <= 300 ms and cache hit ratio >= 90% within 1 minute of steady traffic Given cache service failure When resolving Then engine falls back to live resolution with p95 <= 300 ms and logs METRIC_CACHE_MISS or METRIC_CACHE_ERROR
Regression Test Harness for New Metric Introductions
Given a new metric FirstTimeDonors is registered When CI runs Then fixtures auto-generate preset mappings and compare outputs against golden files across test tenants Given a metric definition changes When CI runs Then the pipeline fails if any scope outputs differ from approved snapshots until snapshots are explicitly updated Given a computed metric is added When CI runs Then allowed/blocked formula tests execute, PII access attempts are rejected, and merges are denied on violation Given schema version increments When CI runs Then migration tests verify backward compatibility for all stored configs in staging Given a new metric is introduced When smoke tests run Then poster renderer and sharelink service render for Board, Sponsor, and Public presets without errors
PII Redaction & Safe Defaults
"As a Data Steward, I want automatic PII redaction based on audience so that no sensitive data is exposed in public or sponsor materials."
Description

Establish an allowlist-based privacy layer that automatically redacts or aggregates personally identifiable information for Sponsor and Public scopes while preserving operational detail for Board where permitted. Redaction strategies include suppression, bucketing, and hashing/initials, with clear fallback to counts and summaries instead of row-level data. A runtime guard blocks rendering or sharing if PII fields are detected outside the allowlist, and a static template scan flags risky components at configuration time. Consent flags and jurisdictional rules (e.g., GDPR, CCPA) are respected, and policy tooltips explain what is shown or hidden to prevent user mistakes.

Acceptance Criteria
Scope-Based Allowlist Enforcement
Given the Sponsor scope preset and a poster template bound to fields [full_name, email, phone, city, total_donations] When the poster is rendered or exported (PDF/PNG/CSV) Then only fields in sponsor_allowlist are visible; full_name/email/phone are not present in raw or obfuscated form unless explicitly allowed And a regex scan of the output detects zero email or phone patterns And the Board scope of the same template renders all allowlisted operational fields without redacting permitted ones
Redaction Strategies and Fallback Aggregation
Given the Public scope and row-level data with fields [full_name, email, phone, address, donation_amount] When the privacy layer applies redaction strategies Then names are reduced to initials or suppressed; emails and phone numbers are replaced with irreversible hashes; addresses are bucketed to city or ZIP3; and donation_amount is binned into defined ranges And if any grouping or series would result in fewer than 5 distinct individuals, Then the view collapses to aggregate counts and summaries only (no row-level)
Runtime Guard Blocks Unsafe Render and Share
Given any scope where a component references a field outside that scope’s allowlist (e.g., person.email in Sponsor) When the user attempts to render the poster or generate a sharelink Then rendering is blocked and a non-dismissible error banner identifies the offending field(s) and component(s) And sharelink generation is disabled until the violation is resolved And no partial render or cached artifact is produced
Static Template Scan Flags Risky Components
Given a Campaign Architect saves a template for Sponsor or Public scope When the static scanner evaluates bindings and component types Then any binding to PII fields outside the scope allowlist is flagged with severity=High and exact references listed And the template cannot be published for that scope until all High findings are resolved And findings are recorded with timestamp, author, and template version
Consent and Jurisdictional Rule Compliance
Given contacts with consent=false or consent_scope!="public" and/or jurisdiction in [EU, UK, CA-BC, CA-Quebec, US-CA] When rendering Sponsor or Public scopes Then all PII for such contacts is suppressed regardless of allowlist And in Board scope, PII for these contacts is shown only if consent=true and policy for that jurisdiction permits; otherwise redacted And the output includes no data transfers that violate GDPR/CCPA flags (validated by field-level policy engine)
Policy Tooltips Clarify Visibility and Redactions
Given any redacted or aggregated field in the poster UI When the user taps the policy info icon Then a tooltip explains what is shown, what is hidden, and why (scope, consent, jurisdiction, rule name) And the tooltip contains no PII and links to the privacy/help page And accessibility checks pass (keyboard focusable, aria-describedby) across mobile and desktop
Safe Defaults for New Sponsor/Public Scopes
Given a user creates a new scope using the Sponsor or Public preset When no manual privacy changes are made Then the scope defaults to strict allowlist mode with all row-level components disabled and aggregation thresholds enabled (k=5) And attempts to add PII-bound fields prompt a confirmation modal with policy summary; declining keeps them excluded And saving the scope generates a safe sharelink that, when opened unauthenticated, reveals no PII
Custom Scope Builder & Save/Load
"As a Campaign Architect, I want to build and save custom scopes so that I can reuse safe configurations across events and campaigns."
Description

Deliver a guided builder to create custom scopes by starting from a preset or from scratch and adjusting metric sets, groupings, labels, and privacy options. Users can name, describe, and save scopes, clone existing ones, and set sharing permissions (personal, campaign, or organization). Saved scopes are versioned with change notes, validated against the rules engine and privacy layer, and can be set as campaign defaults. The system supports conflict resolution when underlying metrics change and provides non-breaking migrations for existing scopes.

Acceptance Criteria
Build Custom Scope From Preset
Given a Campaign Architect opens the Custom Scope Builder and selects a preset (Board, Sponsor, or Public), When they add/remove metrics, change grouping levels, relabel metrics, and toggle privacy options, Then the live preview reflects the exact selection, ordering, labels, and privacy masks and the Save action remains disabled until validation passes. Given unsaved changes exist, When the user attempts to navigate away, Then a confirmation prompt warns of losing changes and offers Save/Discard/Cancel options. Given the user selects Reset to Preset, When they confirm, Then all edits revert to the selected preset baseline with no residual overrides.
Save Scope With Metadata and Permissions
Given a valid configuration passes validation, When the user enters a required Name and optional Description and selects a sharing permission (Personal, Campaign, Organization), Then Save creates a new scope with a unique ID and visibility matching the chosen permission. Given the chosen Name conflicts within the selected permission boundary, When the user attempts to save, Then the system blocks the save and displays a clear uniqueness error indicating the conflicting scope. Given a scope is saved, When scopes are listed and filtered by visibility, Then the new scope appears under the correct filter and is searchable by Name.
Clone and Modify Existing Scope
Given an existing saved scope, When the user selects Clone, Then a new draft is created with identical metrics, groupings, labels, privacy options, and permission, and the Name is prefilled as "<Original Name> (Copy)". Given a clone draft, When the user edits and saves, Then a new scope ID is created independent of the original and no posters referencing the original are altered. Given the user lacks organization-wide permission, When attempting to clone an Organization-visible scope, Then the Clone action is disabled with an explanatory tooltip.
Versioning and Change Notes
Given a saved scope is edited, When the user saves changes after entering a required Change Note (minimum 5 characters), Then a new version is created (incrementing patch by default) and a complete read-only history is retained. Given version history exists, When a user selects a prior version and clicks Restore, Then a new latest version is created that copies the prior state and becomes the active version. Given sharelinks are generated from a scope, When no version is pinned, Then links resolve to the latest passing version; When a poster pins a version, Then links resolve to the pinned version.
Validation Against Rules Engine and Privacy Layer
Given the configuration includes PII or restricted metrics while permission is Campaign or Organization, When validation runs (auto on change or manual), Then save is blocked and offending fields are flagged inline with reasons and remediation suggestions. Given all rules pass, When the user saves and generates a sharelink, Then no PII fields are present in the payload or rendering for non-personal scopes and a rules-engine pass is logged. Given an invalid expression or grouping is configured, When validation runs, Then descriptive errors identify the exact field and the Save action remains disabled.
Set Campaign Default Scope
Given a user with Campaign Admin rights, When they set a saved scope with Campaign or Organization visibility as the campaign default, Then new posters created in that campaign preselect this scope by default. Given a campaign already has a default, When a new default is set, Then the prior default is superseded and logged; existing posters are not changed unless explicitly updated by the user. Given a scope with Personal visibility, When attempting to set it as a campaign default, Then the action is blocked with an explanation of the visibility requirement.
Metric Changes: Conflict Detection, Resolution, and Non-Breaking Migration
Given underlying metric definitions are updated (rename, deprecate, data type change), When the system detects impacted saved scopes, Then owners are notified and the scopes are marked "Needs Review" without breaking existing posters. Given the migration wizard is opened for an impacted scope, When auto-mapping is possible, Then suggested replacements are applied for renamed metrics, deprecated metrics are dropped with explanations, and unresolved conflicts are highlighted for user selection. Given conflicts are resolved and saved with a Change Note, When the new version is created, Then existing posters using a prior version continue to render until owners opt to update, and a migration report is attached to the version history.
Scope-bound Sharelink Generation & Access Controls
"As a Campaign Architect, I want to generate secure sharelinks bound to a scope so that I can safely share posters externally without risking data leakage."
Description

Enable one-click generation of secure sharelinks that are cryptographically bound to a selected scope so the rendered poster cannot exceed its allowed metrics. Links support expiration, single-use tokens, optional password protection, and immediate revocation. View analytics (views, last accessed) are recorded without PII, and links can be created in public-embed or print-ready modes. The service rate-limits requests, signs payloads, and enforces CSP and referrer policies to reduce leakage when embedding on third-party sites.

Acceptance Criteria
One-Click Scope-Bound Sharelink Generation
Given a Campaign Architect selects a scope preset or custom scope and options (mode, expiration, single-use, password-protection) When they click Generate Sharelink Then a unique sharelink URL is returned within 2 seconds And the link metadata persists with scope_id, mode, expiration_at (if set), single_use flag, password_protected flag, status "active" And the URL contains no PII and is <= 512 characters And generating the same link again produces a distinct URL
Scope Enforcement at Render
Given a sharelink bound to a scope that permits a defined set of metrics and groupings When the poster is rendered via this sharelink Then only the permitted metrics and groupings appear; all hidden metrics are absent from the DOM and any underlying API responses And adding or modifying query parameters to request additional metrics results in HTTP 403 with no additional data rendered And the rendered content is identical regardless of viewer authentication or role (sharelink cannot elevate scope)
Expiration and Single-Use Enforcement
Given a sharelink configured with expiration T and single_use=true When the link is accessed before T Then the first successful render returns HTTP 200 and immediately marks the link as consumed And any subsequent access returns HTTP 410 Gone And for two concurrent requests, only one returns 200; the other returns 410 within 1 second When the link is accessed at or after T Then the response is HTTP 410 Gone
Password-Protected Access Gate
Given a sharelink configured with password protection When accessed without a valid password Then the response is HTTP 401 Unauthorized and no poster content is returned When the correct password is supplied over HTTPS Then the poster renders with HTTP 200 And incorrect passwords return HTTP 401 without revealing which part failed And the password is never present in the URL or response body
Immediate Revocation Propagation
Given an active sharelink When an admin revokes the link Then any subsequent access attempts return HTTP 410 Gone within 5 seconds globally And refreshing an already open poster page causes data re-fetch to fail with HTTP 410 And the link status updates to "revoked" with timestamp
Non-PII View Analytics Recording
Given analytics are enabled When a sharelink is successfully rendered Then view_count increments by 1 and last_accessed_at updates to the current UTC timestamp And the persisted analytics schema contains only link_id, view_count, last_accessed_at (no IP address, user agent, referrer, or other PII) And an admin report displays view_count and last_accessed_at for the link
Embedded Security Controls
Given a sharelink URL and token When any part of the token, scope, or mode is tampered with Then signature verification fails and the response is HTTP 403 without rendering content When the poster is loaded in public-embed mode Then responses include Content-Security-Policy and Referrer-Policy headers, and no network requests are made to origins outside the configured allowlist When an origin exceeds the configured rate limits (e.g., 10 requests/min per IP or 5 requests/min per sharelink) Then subsequent requests receive HTTP 429 Too Many Requests with a Retry-After header
Live Preview & Poster Rendering Binding
"As a Campaign Architect, I want a live preview of the scoped poster so that I can verify exactly what recipients will see before sharing."
Description

Provide a real-time preview that reflects the active scope’s rules exactly as the final poster will render, including metric totals, groupings, labels, and hidden elements. The preview integrates with the Impact Board renderer, highlights items excluded by privacy rules, and shows warnings when a configuration would produce empty or ambiguous visuals. Performance is optimized with incremental data fetching and caching to keep mobile interactions responsive, and an offline-safe snapshot supports last-mile printing when connectivity drops.

Acceptance Criteria
Parity Preview for Board/Sponsor/Public Presets
Given a populated Impact Board dataset and the Board, Sponsor, and Public scopes are available When the user toggles between presets and exports the poster (PNG/PDF) Then the on-screen preview and the exported poster match by: identical metric totals and labels; identical group ordering and headings; identical visibility of sections; and no hidden elements rendered And a semantic snapshot comparison reports 0 diffs excluding timestamps and file metadata
Privacy Exclusion Highlighting Without Leakage
Given records contain PII fields or are flagged private by scope rules When a scope that hides these fields is active Then the preview displays non-exported exclusion indicators (badge + count) adjacent to affected items And the exported poster and sharelink contain zero PII fields and no exclusion indicators And an automated scan confirms hidden fields are absent from markup and rasterized output
Empty or Ambiguous Configuration Warnings
Given scope filters yield no visible metrics or produce duplicate/empty group labels When the preview is computed Then a warning banner appears with a specific code (EMPTY_SCOPE or AMBIGUOUS_GROUPING) and a plain-language description And the Export action is disabled with a tooltip explaining resolution steps And clearing the condition removes the warning and re-enables Export
Responsive Incremental Rendering on Mobile
Rule: Time from scope toggle to first visual change ≤ 150 ms at P95 on defined mid-tier mobile profile Rule: Time to preview stabilization (all sections rendered) ≤ 500 ms at P95 and ≤ 1200 ms at P99 for datasets up to 5,000 records Rule: Subsequent scope switches achieve ≥ 80% cache hit rate and ≤ 300 KB network transfer at P95 Rule: Interaction smoothness ≥ 45 FPS with jank < 10% during preview updates
Offline-Safe Snapshot and Print
Given a preview is fully rendered while online When the user saves a snapshot and loses connectivity Then an offline poster package (≤ 5 MB) including required fonts and assets is stored locally And opening the package offline renders identically to the online preview and supports system print dialogs And the footer includes Snapshot Time and Scope Name And on reconnection the user can refresh the snapshot manually
Unified Renderer Binding and Parity Tests
Rule: Preview and export use the same Impact Board renderer module and configuration (single code path) Rule: Automated parity tests across ≥ 20 fixture datasets and ≥ 6 scope variations report 0 snapshot diffs between preview and exported artifacts Rule: Any renderer change gates on passing preview-vs-export parity checks in CI

SafeAggregates

Automatically protects privacy by suppressing small cells, rounding sensitive counts, and disabling drilldowns that could re-identify volunteers. A subtle “privacy applied” badge builds trust with stakeholders. Share meaningful progress without risking individual exposure.

Requirements

Small-Cell Suppression
"As a program lead, I want small counts hidden in summaries so that individual volunteers cannot be identified."
Description

Automatically suppress the display and export of aggregate metrics when the contributing unique individual count falls below a configurable threshold (default k=5). Applies primary and complementary suppression to prevent back-calculation from totals and subtotals. Works across Impact Board tiles, charts, leaderboards, and CSV/PDF exports. Replaces suppressed values with a neutral placeholder and explanatory tooltip, without revealing exact thresholds. Implemented in the analytics query layer and aggregation cache so protections persist across mobile and web views. Ensures consistent behavior regardless of filters, date ranges, or segmentations.

Acceptance Criteria
Impact Board Tile Suppresses Low-Count Metric
Given k=5 and a metric tile showing unique volunteer signups for This Week And the contributing unique individual count is 3 When the tile renders on web and mobile Then the numeric value is replaced with the placeholder "—" And a tooltip is available on hover/tap explaining that privacy protections were applied without revealing any numeric thresholds And drilldown and copy/share actions for that value are disabled And a full page/app refresh shows the same suppressed state
Chart Breakdown Applies Complementary Suppression
Given k=5 and a bar chart breaking out signups by neighborhood with values A=9, B=4, C=7, Total=20 When the chart renders Then primary suppression hides B and shows the placeholder "—" with a privacy tooltip And complementary suppression additionally hides either the Total or one of {A, C} so that B cannot be exactly derived And no suppressed cell can be solved exactly from any combination of displayed totals/subtotals And the selection of complementary cells to suppress is deterministic for identical inputs And tooltips for all suppressed cells contain no numeric characters or threshold values
Filter and Date Range Consistency
Given k=5 and a leaderboard of canvassers filtered to Last 30 Days And Canvasser X has a unique interaction count of 6 When the leaderboard renders Then X's row displays 6 (unsuppressed) When the user changes the filter to Last 7 Days and X's unique count is 3 Then X's value is replaced with the placeholder "—" and a privacy tooltip appears And switching back to Last 30 Days restores the value 6 without showing any intermediate unsuppressed value for the Last 7 Days view And the same suppression/unsuppression behavior is consistent on both web and mobile views
CSV and PDF Exports Respect Suppression
Given a report where one or more cells are suppressed in the UI due to k-threshold rules When the user exports the report to CSV and PDF Then each suppressed cell in the export contains the placeholder "—" instead of the raw number And no suppressed raw values appear anywhere in the export (file body, hidden data, metadata, or embedded objects) And totals/subtotals in the export apply complementary suppression consistent with the UI so no suppressed value can be back-calculated And the export includes a non-numeric note indicating that privacy protections were applied without revealing exact thresholds
Configurable Threshold Overrides Default
Given the default suppression threshold k=5 is active When an authorized user updates the threshold to k=8 Then all tiles, charts, leaderboards, and exports immediately apply k=8 after cache invalidation And UI text/tooltips do not display or imply the numeric threshold And removing the override reverts to the default k=5 And the applied threshold value is consistently enforced across web and mobile
Aggregation Cache Enforces Suppression
Given cached aggregation results exist for a segment with count >= k When a subsequent request queries a narrower segment where the unique count < k and cache keys overlap Then the response returns a suppressed value (placeholder and tooltip), not a leaked unsuppressed number from cache And cache keys incorporate filters, segmentations, date ranges, and the effective k value so suppressed vs. unsuppressed states do not collide And repeated requests with identical parameters return the same suppression state deterministically
Placeholder, Tooltip, and Drilldown UX
Given any surface (tile, chart, leaderboard) where a value is suppressed When the user focuses, hovers, or taps the suppressed value Then the placeholder "—" is displayed and remains consistent across surfaces And a tooltip or accessible hint reads "Value hidden for privacy" (or equivalent) and contains no numeric characters or threshold values And keyboard and screen-reader users can access the tooltip/hint And drilldown, copy, and detail actions are disabled for suppressed values
Privacy-Preserving Rounding
"As a communications coordinator, I want metrics rounded consistently so that I can share progress safely without revealing exact participation."
Description

Round sensitive counts to a policy-defined base (e.g., 5 or 10) after suppression, with consistent rules across widgets, exports, and time windows. Apply sticky rounding to avoid revealing exact changes through small deltas over time. Ensure percentages and rates are derived from rounded, non-suppressed denominators; if a denominator is suppressed, hide the derived metric. Provide safe rounding presets per metric type (people counts, hours, donations). Integrates with the Impact Board renderer and export services to ensure uniform output.

Acceptance Criteria
Uniform Rounding Across UI and Exports
Given a policy rounding base of 5 and a non-suppressed count of 12 When the value is rendered in any Impact Board widget and exported as CSV, PDF, and PNG Then the displayed/exported value is 10 in all outputs and the values match exactly Given a policy rounding base of 5 and a non-suppressed count of 13 When the value is rendered in any Impact Board widget and exported as CSV, PDF, and PNG Then the displayed/exported value is 15 in all outputs and the values match exactly Given any metric and output channel When a policy base is changed from 5 to 10 and the view is refreshed Then all outputs reflect the new base within one refresh and no channel shows the prior rounded value
Sticky Rounding Over Time Series
Given rounding base B=5 and last disclosed rounded value R=10 for a metric When true counts vary within the range [8..12] across consecutive daily windows Then the disclosed rounded value remains R=10 for all those windows Given rounding base B=5 and last disclosed rounded value R=10 for a metric When cumulative absolute change since the last disclosure reaches ≥ B (e.g., true count hits 15) Then the disclosed rounded value updates to the nearest base (15) and remains sticky until the next cumulative change ≥ B Given a user reloads the same report over multiple days with small deltas < B When viewing the time series Then no oscillation of the rounded value occurs due to minor day-to-day changes
Derived Metrics Use Rounded, Non-Suppressed Denominators
Given a percentage metric where numerator and denominator are both non-suppressed When computing the percentage Then the percentage is calculated using the rounded numerator and the rounded denominator only Given a percentage or rate where the denominator is suppressed by policy When rendering the derived metric in widgets and exports Then the derived metric value and label are hidden and a standard suppression indicator is shown Given any derived metric When validating data lineage Then no derived metric is computed using an unrounded denominator
Safe Rounding Presets by Metric Type
Given default SafeAggregates presets are enabled When creating a widget for People counts Then the rounding base defaults to 5 Given default SafeAggregates presets are enabled When creating a widget for Volunteer hours Then the rounding base defaults to 5 Given default SafeAggregates presets are enabled When creating a widget for Donation dollars Then amounts are rounded to the nearest $10 by default Given an administrator selects a different safe preset for Donations (e.g., $100) When reports and exports are rendered Then donation amounts are rounded to the nearest $100 uniformly across UI and exports Given a user attempts to set a rounding base not in the approved safe presets for a metric type When saving the policy configuration Then the system rejects the change with a validation error message and no policy update occurs
Shared Rounding Service Integration
Given a dataset and an active rounding policy When the Impact Board renders a view and an export (CSV/PDF/PNG) is generated for the same view Then the rounded values match exactly across UI and exports for 100% of displayed metrics Given a policy update to change the rounding base for People counts When the renderer and export service are invoked after the change Then both use the updated policy in the same request cycle and no divergence is observed Given test fixtures with known inputs When running renderer and export code paths Then the rounded outputs are identical across channels for all fixtures
Suppression Before Rounding
Given a suppression threshold k=3 and a rounding base of 5 When a cell value equals 1 or 2 Then the cell is suppressed and no rounded number is displayed in UI or exports Given the same policy When a cell value equals 12 Then the value is not suppressed and is rounded to 10 for display and export Given a table with mixed suppressed and non-suppressed cells When exporting or rendering Then no suppressed cell displays a rounded value and suppression indicators are consistent across outputs
Drilldown Guardrails
"As a volunteer organizer, I want drilldowns to be blocked on small groups so that I don’t accidentally expose individuals."
Description

Disable record-level drilldowns, quick-filters, and slice exports when the resulting cohort would drop below the privacy threshold. Present a disabled state with a brief, non-technical explanation and a link to the privacy explainer. Enforce guardrails at both the UI and API layers to prevent circumvention via direct URLs or export endpoints. Log attempted restricted actions for audit without capturing sensitive content. Applies uniformly across mobile, web, and shared links of the Impact Board.

Acceptance Criteria
Drilldown Disabled Below Threshold (UI)
Given an Impact Board slice with a cohort count below the configured privacy threshold When the user attempts record-level drilldown via tap/click/keyboard Then the drilldown control is disabled and no record list renders And a disabled-state message with a visible “Privacy applied” badge is shown And a link labeled “Learn how we protect privacy” opens the privacy explainer And the control exposes aria-disabled=true and an accessible description of why it is disabled
Quick-Filter Guardrail Blocks Small Cohorts
Given the user selects quick-filters that would reduce the cohort below the configured privacy threshold When the user applies the filters Then the filters are not applied and the prior results remain visible And an inline message explains that privacy protections prevent showing such a small group And a “Privacy applied” badge and link to the privacy explainer are present And no partial record-level data or counts are displayed
Slice Export Guardrails (UI and API)
Given a metric slice or filter set with cohort size below the configured privacy threshold When the user opens export options in the UI Then export actions are disabled with a privacy message and badge And no file is generated Given a request to the export API with parameters resolving to a cohort below threshold When the request is made with valid authentication Then the API responds 403 Forbidden with error code PRIVACY_THRESHOLD and a user-safe message And the response contains no row-level data or file content
Direct URL and API Circumvention Prevented
Given a direct deep link or constructed URL to a record-level view that resolves to a cohort below the configured privacy threshold When the URL is loaded on web, mobile, or a shared link Then the server blocks the request and the UI shows a privacy message with badge and explainer link And no record-level fields, IDs, or counts are returned in the response or rendered Given a call to a record-list API with parameters resolving below threshold When the call is made Then the API returns 403 Forbidden with error code PRIVACY_THRESHOLD and no record payload
Non-Technical Explanation and Privacy Badge
Given any guardrail is triggered (disabled drilldown, blocked filter, or blocked export) When the message is displayed Then the copy is non-technical, ≤140 characters, and clearly explains that privacy protections prevent showing small groups And the “Privacy applied” badge is visible adjacent to the message And the link text “Learn how we protect privacy” opens the privacy explainer page
Uniform Enforcement Across Web, Mobile, and Shared Links
Given the same below-threshold cohort scenario When accessed via web app, mobile app, and public shared Impact Board link Then drilldown, quick-filters, and exports are disabled/blocked consistently across all surfaces And the same badge, message, and explainer link behavior is present on each surface And API responses are identical regardless of client
Audit Logging Without Sensitive Content
Given any restricted action is attempted and blocked by guardrails When the event is logged Then the audit entry captures timestamp, user or session identifier (if available), action type, surface (web/mobile/shared), endpoint/route, and reason code PRIVACY_THRESHOLD And no record IDs, names, emails, phones, free-text filter values, or row-level payloads are logged And the audit entry is available to authorized admins within 60 seconds
Privacy Badge & Disclosure
"As a stakeholder, I want a clear indicator when privacy protections are applied so that I can trust the shared numbers."
Description

Display a subtle, consistent "privacy applied" badge on any widget or export where suppression or rounding altered the data. Provide an accessible tooltip or tap-through that explains, in plain language, that privacy protections are active without disclosing specific thresholds. Include the badge in exported images/PDFs and shared links to build trust with external stakeholders. Adhere to brand and accessibility guidelines (contrast, screen-reader labels) and localize the badge text as needed.

Acceptance Criteria
Widget badge visibility for suppressed/rounded data
Given a widget that displays values without any SafeAggregates transformations, When the widget renders, Then the privacy badge is not present. Given a widget where SafeAggregates has suppressed small cells or rounded any displayed value, When the widget renders, Then a privacy badge appears in the widget header adjacent to the title with aria-label "Privacy protections applied". Given a widget with the badge displayed, When the underlying data updates such that no suppression or rounding is applied, Then the badge is removed on the next render without requiring a page reload. Given a widget where drilldown is disabled due to privacy risk, When the widget renders, Then the privacy badge is displayed even if displayed values are unchanged.
Accessible disclosure without revealing thresholds
Given a user hovers, focuses via keyboard, or taps the badge, When the disclosure opens, Then it states that privacy protections are active and that small counts may be suppressed and/or values rounded and that certain drilldowns may be disabled. Then the disclosure contains no numeric thresholds, sample sizes, k-values, or algorithm parameters. Then the disclosure text has a Flesch–Kincaid grade level of 8.0 or lower. Then the disclosure is associated to the badge via aria-controls/aria-describedby, is dismissible via Escape key and outside click/tap, and returns focus to the triggering badge.
Exports and shared links carry the badge
Given a widget with a visible privacy badge, When the user exports the widget to PNG or PDF, Then the exported file includes the badge icon and the localized "privacy applied" label. Given a dashboard viewed via a public shared link or embedded view, When the view loads, Then all affected widgets display the privacy badge. Given an export executed in light or dark theme, When the export completes, Then the badge meets required contrast ratios in the exported artifact. Given an export from a widget without privacy transformations, When the export completes, Then no privacy badge appears in the artifact.
WCAG AA accessibility of badge and disclosure
Then badge text meets contrast ratio ≥ 4.5:1 and badge icon/non-text elements meet contrast ratio ≥ 3:1 against their backgrounds. Then the badge is reachable via keyboard in logical order immediately after the widget title; activating with Enter or Space opens the disclosure. Then screen readers announce "Privacy applied. Data may be rounded or small counts suppressed." when the badge receives focus. Then the disclosure is keyboard navigable, trap-free, and is announced with an accessible name and description; pressing Escape closes it.
Localization and RTL support for badge and disclosure
Given the app locale is set to a supported language, When the widget renders, Then the badge label and disclosure copy display in that locale. Given the app locale is unsupported or a translation is missing, When the widget renders, Then the badge and disclosure fall back to English. Given a right-to-left locale, When the widget renders, Then the badge and disclosure mirror alignment and respect RTL reading order. Given the user changes locale at runtime, When the UI re-renders, Then the badge and disclosure update to the new locale without a full page reload.
Brand-consistent, subtle presentation across breakpoints
Then the badge uses only design-system tokens for color, spacing, typography, and iconography (no hardcoded values). Then the badge sits to the right of the widget title with 8–12px spacing, does not overlap content, and occupies ≤ 5% of the widget header width. Then the badge meets minimum touch target size of 24×24 px on mobile and scales without clipping at all supported breakpoints.
Admin Policy Controls & Templates
"As an org admin, I want to set privacy thresholds and presets so that the policy matches our risk tolerance and funder requirements."
Description

Offer an admin UI to configure privacy policies: threshold k, rounding base, protected dimensions (e.g., geography granularity, demographics), and role-based exceptions. Provide vetted templates (Standard, High Sensitivity) and per-Impact Board overrides with clear effective dates. Validate configurations to prevent unsafe combinations and show a live preview of how aggregates will render. Version and audit all policy changes with who/when/what metadata and safe rollback to prior versions.

Acceptance Criteria
Configure Threshold and Rounding Base
Given I am an Org Admin on the SafeAggregates policy page When I set "Threshold k" to 10 and "Rounding base" to 5 and click Save Then the policy saves successfully and the summary shows "k=10, rounding=5" And aggregate displays use k>=10 and round counts to the nearest 5. Given the active policy has k=10 and rounding=5 When an Impact Board would display a count of 47 Then the displayed count is 45. Given I enter a Rounding base not in [1, 5, 10, 20, 50] When I click Save Then I see a blocking validation error "Rounding base must be 1, 5, 10, 20, or 50" and the policy is not saved. Given I enter a Threshold k outside 5–500 When I click Save Then I see a blocking error "k must be between 5 and 500" and the policy is not saved.
Define Protected Dimensions and Enforce Drilldown Suppression
Given I select protected dimensions: Geography minimum granularity = County and Demographics = Age band, Gender When I open an Impact Board grouped by ZIP or attempt to drill down below County Then drilldown controls are disabled And any cell where n<k is hidden with a suppression indicator. Given I deselect Demographics: Gender from protected dimensions When I refresh the same view Then drilldowns by Gender are enabled only for cells meeting k And rounding still applies to all shown counts. Given a user appends URL parameters to force a disallowed drilldown When the view loads Then the server returns 403 Forbidden and logs the attempt with user ID and timestamp.
Role-Based Exceptions and Access Enforcement
Given I grant the Data Steward role an exception "Allow drilldowns in Admin Console" When a user with the Data Steward role views an Impact Board in the Admin Console Then drilldowns are enabled where each resulting cell meets k And public/shared views remain suppressed per policy. Given a Data Steward attempts to export data When they export a table including small cells Then any cell where n<k remains suppressed in the export. Given I remove the exception from the Data Steward role When the user refreshes Then drilldowns are disabled immediately according to the base policy. Given a user without exceptions attempts to access an exception-only endpoint When the request is made Then access is denied and the event is written to the audit log.
Apply Policy Templates with Editable Defaults
Given templates exist: Standard (k=10, rounding=5, protected dims: County+, Demographics: Age band, Gender) and High Sensitivity (k=25, rounding=10, protected dims: State+, Demographics: All) When I select High Sensitivity Then the form auto-populates those values And the Template indicator shows "High Sensitivity". Given I modify any populated field after applying a template When I change k from 25 to 20 Then the Template indicator updates to "Custom". Given a template is selected When I click "Reset to Template" Then the fields revert to the template defaults. Given I save a policy derived from a template When the save succeeds Then the created version records the template name in metadata.
Per-Impact Board Overrides, Effective Dates, and Live Preview
Given a global policy exists and I open Impact Board "Northside Drive" When I enable "Override policy for this board" Then policy fields for that board become editable without altering the global policy. Given I set an effective start date/time in the future (e.g., 2025-08-15 09:00 local) When I click Save Then the override is scheduled with status "Pending" and shows the effective timestamp. Given a pending override exists When I open the Live Preview and toggle "Preview scheduled policy" Then the preview renders suppression, rounding, and drilldown states exactly as they will appear after the effective time And the "privacy applied" badge is visible on the preview. Given I remove an override and Save When I revisit the board Then it inherits the global policy and the preview reflects the global settings.
Validation Prevents Unsafe Combinations
Given I set k=4 When I click Save Then I see a blocking error "k must be between 5 and 500" and the policy is not saved. Given Minimum geography granularity is County When I enable drilldown by City or ZIP Then I see a blocking error "Drilldown granularity cannot be finer than County under current policy". Given I attempt to allow exports that bypass suppression under any role-based exception When I click Save Then I see a blocking error "Exports must always respect suppression; exceptions cannot bypass it". Given any blocking validation error is present When I attempt to Save Then the Save action is disabled and no new version is created.
Policy Versioning, Audit Trail, and Safe Rollback
Given I save a policy change Then a new immutable version is created with version ID, author, UTC timestamp, scope (Global or Board), and a structured diff of changed fields. Given I open the Policy Audit Log and filter by Board "Northside Drive" When I view results Then I see every version for that board with who/when/what metadata. Given I select a prior version and click Rollback When I confirm Then the system creates a new version that re-applies the selected settings And audit metadata records "rolled back to <version ID>" with the actor and timestamp. Given the audit write fails during Save or Rollback When the operation is attempted Then the change is aborted and the user sees "Unable to persist audit; no changes applied".
Privacy Audit Logging & Reports
"As a compliance officer, I want reports of where privacy protections triggered so that I can demonstrate due diligence."
Description

Capture structured events whenever suppression, rounding, or drilldown blocking occurs, including widget ID, query context (non-identifying), policy version, and viewer role. Provide an admin-only dashboard and weekly export summarizing where protections were applied, frequency, and any attempted restricted actions. Redact or hash identifiers to avoid logging sensitive data. Enable correlation with policy changes to support governance reviews and threshold tuning.

Acceptance Criteria
Log Event Structure for Privacy Protections
Given a SafeAggregates operation triggers suppression, rounding, or drilldown blocking When the result is generated Then an audit event is written within 2 seconds containing fields: event_id (UUIDv4), event_type ∈ {suppression, rounding, drilldown_block}, widget_id, policy_version, viewer_role, timestamp (UTC ISO-8601), query_context (non-identifying: metric_name, geographic_level, time_bucket), protection_parameters (e.g., threshold_value, rounding_base), outcome ∈ {applied, blocked} And the logging sink acknowledges write (2xx) Given the logging sink is unavailable When the event is enqueued Then it is retried with exponential backoff for up to 24 hours and surfaced in system health metrics And no user-facing operation reveals logging failures Given duplicate operations occur within 1 second for the same widget_id and event_type When events are written Then each event has a unique event_id and idempotency prevents duplicate payloads
Non-Identifying Context Redaction and Hashing
Given any audit payload is prepared When identifiers are processed Then only the following may appear in cleartext: widget_id, viewer_role label, policy_version, non-identifying query_context And user_id, email, device_id, IP, raw query text, and free-text filters are excluded or replaced with SHA-256 hash using a rotating daily salt; include salt_version only Given 1,000 randomly sampled audit events When scanned by automated DLP rules (email, phone, SSN, name patterns) Then 0 high-severity findings and ≤1 low-severity false positives are detected Given viewer roles exist When logged Then only the role label is stored; no role member identifiers are present
Admin-Only Privacy Audit Dashboard
Given an authenticated Admin user opens the Privacy Audit dashboard When applying filters by date range, event_type, widget_id, policy_version, viewer_role Then counts, daily trend, top widgets, and event table render with no PII And the initial load completes in ≤2 seconds for a 30-day window with ≤1,000,000 events Given a non-admin user attempts to access the dashboard When the request is made Then a 403 is returned and no data is leaked And an audit event with event_type "audit_access_denied" is recorded without sensitive fields Given an Admin exports the current filtered view When CSV download is requested Then up to 100,000 rows are delivered and totals match the on-screen aggregates within 0.1%
Weekly Audit Summary Export
Given it is Sunday 02:00 UTC When the weekly export job runs Then a CSV summary is produced with counts by event_type, widget_id, policy_version, viewer_role, daily breakdown, and attempted restricted actions And the file name includes the ISO week and date range And the export is delivered via a secure channel and available for Admin download Given export delivery fails When retries are attempted Then up to 3 retries with exponential backoff occur; on final failure an alert is sent to Admins and the job status shows "Failed" with reason And an "audit_export" event is logged for success or failure Given an Admin requests an on-demand export for a date range When the export completes Then the contents equal the dashboard totals for the same filters within 0.1%
Correlation of Events with Policy Version Changes
Given multiple policy versions have been deployed When "Show policy changes" is enabled on the dashboard Then charts annotate policy_version change points and metrics can be segmented by policy_version Given a new policy version is activated When the next audit event is written Then the event includes the new policy_version And a "policy_change" audit event is recorded containing policy_version, change_id, deploy_artifact_id (non-identifying), and timestamp Given a weekly export spans multiple policy versions When the file is generated Then per-policy_version summaries are included to support governance review and threshold tuning
Drilldown Block Attempts Monitoring
Given a viewer initiates a drilldown on a protected cell When the system blocks the drilldown Then an audit event is recorded with event_type "drilldown_block", reason_code, widget_id, viewer_role, policy_version, and non-identifying query_context; no row-level identifiers are logged Given more than 50 drilldown_block events occur for the same widget_id within 10 minutes When detected Then the dashboard surfaces an alert badge for that widget and an optional notification is sent to Admins; the threshold is configurable Given an Admin filters by reason_code on the dashboard When results are viewed Then the top reason_codes and their frequencies for the selected range are displayed and exportable
Anti-Differencing Consistency
"As a data analyst, I want aggregates to remain privacy-safe across totals and over time so that no one can infer hidden small groups."
Description

Prevent re-identification via differencing across totals, subtotals, and time series by applying complementary suppression and sticky rounding rules that keep aggregates privacy-safe in combination. Detect risky query sets (e.g., total minus one visible subgroup) and adjust visibility or rounding to avoid inference. Ensure consistency across cached aggregates and rolling time windows so protections do not flicker as counts fluctuate around thresholds.

Acceptance Criteria
Complementary Suppression Prevents Single-Cell Inference
Given a dashboard total T and a breakdown by a single dimension with subgroups S1..Sn, suppression threshold k=5, and rounding base r=5 When any subgroup count si < k Then the system must suppress or coarsen at least two values among {T, S1..Sn} so that no single suppressed cell can be derived by subtraction And if T remains visible, at least two subgroup values must be hidden or coarsened And all displayed non-suppressed values are rounded to the nearest r using sticky rounding rules
Sticky Rounding Across Rolling Time Series
Given a time series metric with previous displayed rounded value V (a multiple of 5) at time t-1 and actual count C at time t And rounding base r=5 and sticky band b=±2 around V When C ∈ [V-2, V+2] Then the displayed rounded value at time t remains V When C ≥ V+3 or C ≤ V-3 Then the displayed rounded value at time t becomes the nearest multiple of r to C (ties away from zero) And across consecutive times t-1..t+1 with counts staying within the sticky band, the rounded value does not change
Drilldown Disabled When It Enables Differencing
Given a protected aggregate view where a dimension breakdown has at least one suppressed subgroup due to k=5 When the user attempts to drill down into that dimension or any child dimension that would reveal all but one subgroup contributing to the total Then the drilldown action is disabled or replaced by an aggregated view that maintains at least two unknown components And a tooltip states "Privacy applied: drilldown limited to prevent re-identification"
Risky Query Set Detection for Overlapping Filters
Given a user session (15-minute window) and two queries Q1 and Q2 that share identical base filters, where Q2 adds exactly one additional restrictive predicate And suppression threshold k=5 and rounding base r=5 When both Q1 and Q2 would return visible counts c1 and c2 such that |c1 - c2| < k Then the system marks the pair as risky and applies protection by either suppressing one response or rounding both to the same multiple of r so that the implied difference cannot reveal a subthreshold count And the protected response includes an indicator that privacy protections were applied
Cache and Cross-Endpoint Consistency of Protections
Given the same aggregate query (metric, dimensions, filters) executed via Dashboard API, Export API, and UI within a 5-minute window And suppression threshold k=5, rounding base r=5, and sticky band b=±2 When the underlying data state is unchanged during the window Then all endpoints return identical protections: same suppression flags for the same cells and the same rounded values for visible cells When underlying data for any affected cell crosses a protection boundary (from <k to ≥k+2 or vice versa, or crosses the sticky band) at time T0 Then all cached entries for that query are invalidated and protections converge across endpoints within ≤60 seconds after T0
Totals, Subtotals, and Time Buckets Remain Non-Differencable
Given a report showing weekly buckets W1..W4 and a 4-week total TW, with suppression threshold k=5 and rounding base r=5 When any Wi is suppressed because Wi < k Then the system must either suppress TW or suppress/coarsen at least one additional Wi so that the suppressed Wi cannot be derived by TW - sum(other visible Wj) And the same rule applies to any concurrently displayed subtotal (e.g., by region), ensuring at least two unknowns exist in any displayed sum equation

Sponsor Gate

Require a lightweight check—email or SMS code, passcode, or domain allowlist—before a sharelink opens. Each viewer is individually marked, aligning the watermark to the verified identity. Keeps links truly “for your eyes only” without IT overhead.

Requirements

Multi-Method Verification Gate
"As an organizer, I want a simple verification step on shared links so that only the right people can view materials without needing IT to set up accounts."
Description

Present a mobile-first gate before any sharelink opens, supporting configurable methods: email one-time code, SMS one-time code, shared passcode, and domain allowlist. Admins choose one or multiple methods per link and set settings such as code length, expiry, max attempts, and "remember this device" duration. Domain allowlist requires verification of an email that matches approved domains to prevent spoofing. The gate runs entirely within GiveCrew’s share workflow with no IT setup, minimizes steps, and handles edge cases (expired codes, carrier delays, blocked SMS, link timeouts) with clear recovery paths.

Acceptance Criteria
Email OTP Verification Success and Watermark Identity Alignment
Given a sharelink configured with Email OTP with code length L and expiry E minutes And max attempts M is configured When a viewer enters an email address and the system sends an OTP And the viewer submits the correct OTP within E minutes and within M attempts Then the gate grants access to the target content And the viewer session is associated with the verified email identity And the content watermark displays the verified email identity And the OTP is invalidated immediately after successful use And a verification success event is recorded with method=email
SMS OTP Delivery Delay, Resend Throttling, and Email Fallback
Given a sharelink configured with SMS OTP (primary) and Email OTP (secondary) And resend throttle R seconds and resend limits of X resends within Y minutes are configured When a viewer requests an SMS OTP Then the system enforces the resend throttle and limits per phone number and device And if delivery failure is reported by the carrier or no delivery is confirmed after T seconds, the UI displays options to Resend and to Try email instead When the viewer switches to Email OTP and verifies successfully within configured limits Then access is granted and any previously issued SMS OTPs for this session are invalidated And a verification outcome is recorded with the actual method used (sms or email)
Shared Passcode Validation, Error Messaging, and Lockout
Given a sharelink configured with a shared passcode and max attempts M with cooldown C minutes When a viewer enters the exact passcode, with case sensitivity enforced as configured Then access is granted and a success event is recorded with method=passcode When a viewer enters an incorrect passcode Then a generic error message is shown that does not reveal passcode rules or strength And the failed attempt count increments And after M failed attempts, the gate locks out the device and IP for C minutes and offers a switch to other configured methods
Domain Allowlist Email Verification and Privacy Preservation
Given a sharelink configured with an email domain allowlist [D1, D2, ...] and Email OTP When a viewer enters an email address Then the system validates that the domain (case-insensitive, unicode-normalized) matches the allowlist before sending an OTP And display names and injection are stripped so only the bare address is evaluated And plus-addressing is accepted if the base domain matches the allowlist And messages never reveal whether a specific mailbox exists; non-allowed domains receive a generic not-allowed notice And access is granted only after OTP verification for an allowed-domain address within configured expiry and attempts limits And a success event records method=email and the matched allowlist domain
Multiple Methods Any-Of Gate and Method Switching
Given a sharelink configured with multiple methods [Email OTP, SMS OTP, Passcode] and settings L (code length), E (expiry), and M (max attempts) When a viewer opens the link Then the gate presents the highest-priority method as configured and provides an option to switch methods And successful completion of any one configured method grants access And switching methods preserves per-method attempt counters and invalidates outstanding OTPs from the previously selected method And settings L, E, and M are enforced per method exactly as configured And the method used is persisted with the session and recorded in analytics
Remember This Device Duration and Revocation
Given Remember this device is enabled with duration D hours for a sharelink When a viewer successfully verifies via any method and opts to remember the device Then a device-scoped token is stored and subsequent opens of the same sharelink on that device within D hours bypass the gate And after D hours or upon link revocation or configuration tightening, the device must re-verify And clearing site/app data removes the token and requires re-verification And the remember token is scoped to the specific sharelink and cannot be used to access other links or devices
Expired Codes, Link Timeout, and Clear Recovery Paths
Given a sharelink with OTP expiry E and link TTL L is configured When a viewer submits an OTP after E minutes have elapsed Then the UI clearly states the code has expired and offers to request a new code without losing the selected method When the sharelink TTL L has elapsed Then access is denied even with a valid code and a link-expired state is shown without leaking content And the UI provides a safe recovery option (e.g., contact owner or request access) if configured, and an option to choose another method where applicable
Identity-Bound Watermarking
"As a campaign lead, I want each viewer’s identity watermarked on shared materials so that leaks can be traced and viewers are reminded to keep items confidential."
Description

Upon successful verification, bind the viewer’s verified identity (matched GiveCrew contact name where available, else verified email/phone) to a dynamic, tamper-evident watermark. Render the watermark across in-app views, downloadable PDFs, images, and live posters accessed via the link, including timestamp, viewer identifier, and link ID. Ensure visibility without obscuring content, support light/dark modes, and apply unique per-viewer tokens to deter screenshots and leaks while reminding viewers the content is private.

Acceptance Criteria
Identity Bound Watermark After Sponsor Gate Verification
Given a viewer successfully verifies via Sponsor Gate (email code, SMS code, passcode, or domain allowlist) When the protected link content loads Then the watermark renders with: viewer identifier (GiveCrew contact full name if matched; else verified email or E.164 phone), ISO8601 timestamp of access, and link ID And the watermark includes a unique per-view token with at least 128 bits of entropy And the token, timestamp, link ID, and viewer identifier are written to an append-only audit log
Watermark Across All Surfaces and Exports
Given a verified viewer opens in-app views, downloads PDFs/images, or opens live posters from the link When any surface renders or exports Then the watermark appears on all pages/frames with identical identity, timestamp, link ID, and per-view token And for multi-page PDFs, the watermark is present on each page And for images/posters, the watermark tiles across the full canvas at 200–300px intervals And downloaded files contain the watermark baked into pixels/vector paths, not as a removable overlay layer
Visibility Without Obscuring Content (Light/Dark Support)
Given the content is viewed in light or dark mode on mobile and desktop When the watermark is rendered Then the watermark adapts for contrast (light on dark, dark on light) achieving at least 3:1 contrast against its immediate background And watermark opacity is between 8% and 16% And no interactive UI element becomes unclickable due to the watermark (overlay ignores pointer events) And primary content remains legible with watermark covering no more than 15% of the viewport area at any time
Tamper-Evident Signature
Given the watermark is generated server-side When it is rendered, it includes a tamper-evident checksum (e.g., HMAC-SHA256) derived from link ID, viewer identifier, per-view token, and timestamp using a server secret, displayed as a short code Then the checksum can be validated by a server tool to confirm authenticity for any exported asset And if validation fails or the checksum is missing, the system records a tamper event tied to the link ID and per-view token
Per-View Token Rotation and Reuse Rules
Given the same verified viewer reloads the link or opens it on another device or browser When the content renders Then a new per-view token is generated for each render And the watermark shows the current token while preserving the same viewer identifier And the audit log links all tokens to the same viewer identifier and link ID
Fallback Identity Resolution
Given the verified identifier matches an existing GiveCrew contact When the watermark is rendered Then it displays the contact’s full name as the viewer identifier And if no contact match exists, it displays the verified email address or E.164-formatted phone used for verification And the identifier displayed exactly matches the value recorded in the audit log
Performance and Fail-Safe Rendering
Given a verified viewer opens the content on a slow or unstable network When rendering the watermark Then watermark generation adds no more than 200ms to initial render time on median mobile hardware And if the watermark service is unavailable, the content does not display; a retry message is shown until a watermark can be applied
Link-Level Policy Manager
"As a staffer, I want to set or adjust the gate policy for each link in a few taps so that I can match friction to the sensitivity of the content."
Description

Provide an admin UI embedded in the share flow to configure gate policies per link: select verification method(s), set allowed domains, define expiration dates and view/device limits, enable/disable "remember device," and choose default org templates. Include previews of the end-user experience, one-tap policy presets (Low, Standard, High), and the ability to revoke or update policies post-share with immediate effect. Integrate with existing sharelink creation, QR generation, and permissions to keep setup under 30 seconds.

Acceptance Criteria
Policy Manager Embedded in Share Flow
- Given a user with Share permission opens the Share dialog for a resource, When they proceed to Create sharelink, Then the Policy Manager step is presented inline before link creation with focus on the first control. - Given a user without Share permission, When they open the Share dialog, Then the Policy Manager is read-only and the Create sharelink action is disabled. - Given the user applies the Standard preset without further edits, When they tap Create link, Then the link is created and a QR code is generated using the configured policy in ≤ 30 seconds total from Share dialog open on a mid-range device over 3G or better. - Given keyboard-only navigation, When tabbing through the Policy Manager, Then all interactive elements are reachable in logical order with visible focus styles and accessible labels.
Verification Methods Selection Per Link
- Given the Policy Manager is open, When the user configures verification, Then the UI supports enabling Email code, SMS code, Passcode, and setting a Domain allowlist. - Given no verification method is enabled, When attempting to save, Then validation blocks progress with Select at least one verification method. - Given Domain allowlist is enabled without Email code, When attempting to save, Then validation blocks with Email verification is required when using domain allowlist. - Given Passcode is enabled, When the user enters a passcode, Then it must be 6–12 characters with at least 1 letter and 1 number; otherwise save is blocked with an inline error. - Given SMS code is enabled, When saving, Then it succeeds only if an org SMS sender profile exists; otherwise show an actionable error with a link to configure SMS. - Given multiple methods are enabled, When an end user opens the link, Then the gate requires each enabled method before granting access.
Expiration and View/Device Limits
- Given the admin sets an expiration date/time, When the selected time is in the past, Then save is blocked with Expiration must be in the future. - Given a valid expiration is set, When the end user opens the link after that timestamp, Then access is denied with Link expired and no gated content loads. - Given a view limit N is set, When the (N+1)st open occurs across any devices, Then access is denied and the event is logged. - Given a device limit D is set, When a (D+1)st unique device attempts first-time access, Then access is denied while previously authorized devices continue until expiration or revoke. - Given both limits are set, When access attempts occur concurrently, Then counters remain consistent and atomic with no overrun. - Given the admin’s timezone differs from UTC, When viewing the policy, Then expiration displays in the admin’s local timezone while persisting in UTC.
Remember Device Toggle Behavior
- Given Remember Device is enabled for a link, When an end user completes verification on a device, Then subsequent opens on the same device within the remember period bypass verification. - Given Remember Device is disabled for a link, When an end user completes verification, Then subsequent opens always require verification. - Given Remember Device is toggled from On to Off after sharing, When a previously remembered device opens the link, Then it must re-verify within 10 seconds of the change propagating. - Given the remember period elapses, When the device opens the link, Then verification is required again.
End-User Experience Preview Accuracy
- Given any change to verification methods, domains, limits, or messages, When the admin views the preview pane, Then the preview updates within 300 ms to reflect the current configuration. - Given Passcode is enabled, When previewing, Then the passcode input shows the same labels and validation as the actual gate. - Given Email code with Domain allowlist is enabled, When previewing, Then the email entry shows domain restriction messaging and rejects disallowed domains. - Given Standard preset is applied, When previewing QR, Then scanning the preview QR in test mode shows a gated flow that matches the previewed steps.
Policy Presets Application (Low, Standard, High)
- Given the admin taps Low preset, Then the form auto-fills: verification = Passcode only (auto-generated 8-char editable), expiration = 30 days, views = unlimited, devices = unlimited, remember device = On, domains = none. - Given the admin taps Standard preset, Then the form auto-fills: verification = Email code, expiration = 14 days, views = 25, devices = 5, remember device = On, domains = optional per org template. - Given the admin taps High preset, Then the form auto-fills: verification = Email code + SMS code, expiration = 7 days, views = 5, devices = 1, remember device = Off, domains = org allowlist if present else required before save. - Given any preset is applied, When the admin edits any field, Then the preset indicator changes to Custom. - Given a preset is re-applied, When confirmed, Then it overwrites conflicting custom values. - Given High preset is applied without an org SMS sender, When attempting to save, Then an error prompts to configure SMS or choose a different preset.
Post-Share Update/Revoke with Immediate Effect
- Given a link has been shared, When the admin updates any policy field, Then the change takes effect for new opens within 10 seconds and is logged with actor, timestamp, and diff. - Given a link has been shared, When the admin taps Revoke, Then the link immediately returns HTTP 410 (Link revoked) for both URL and QR and no gated content loads. - Given an end user is currently viewing content, When the admin tightens the policy, Then the next gated action (refresh or navigation) enforces the new policy and may block access. - Given the policy is loosened (e.g., extended expiration), When the end user opens the link again, Then access reflects the updated policy without issuing a new URL or QR.
Reliable Code Delivery and Retry
"As a volunteer coordinator, I want verification codes to arrive quickly and reliably so that supporters can access materials without getting stuck."
Description

Deliver verification codes via trusted email and SMS providers with templated, branded messages. Implement rate limiting by IP/device/contact, resend throttles, automatic retries with exponential backoff, and link-specific TTLs to reduce abuse and drop-off. Validate phone formatting and international numbers, handle carrier filtering, and provide graceful fallbacks (switch channel, use passcode) if delivery fails. Log events for diagnostics and expose deliverability status to admins.

Acceptance Criteria
Branded Email Code Delivery
Given a viewer requests a verification code via email for a Sponsor Gate link When the email address is valid and not rate-limited Then the system sends a single-use 6-digit code using the configured branded template and trusted email provider And the email subject and body include the organization name and do not expose the full email address (masked) And the event is logged with provider response, request ID, and timestamp And the viewer-facing status shows “Code sent” within 2 seconds of provider acceptance
SMS Delivery with International Number Validation
Given a viewer enters a phone number for SMS verification When the number is parsed and normalized to E.164 Then invalid or unsupported numbers are rejected with a clear error and no send occurs And valid numbers from at least US (+1), GB (+44), IN (+91), AU (+61) are accepted and an SMS with a 6-digit code is sent via the trusted provider And messages over 160 GSM-7 characters are not used; the template remains within 1 segment And the send attempt (success/failure and reason code) is logged
Resend Throttles and Rate Limiting (IP/Device/Contact)
Given any channel (email or SMS) is used to request a verification code When requests exceed policy thresholds Then the system enforces: per-contact max 3 sends in 5 minutes; per-device max 5 sends in 10 minutes; per-IP max 20 sends in 60 minutes And a resend throttle of minimum 30 seconds between sends per contact per channel is enforced And blocked requests return a non-200 with a descriptive, non-enumerating message and the next eligible time And all rate-limit decisions are logged with the applied rule
Automatic Retries with Exponential Backoff and Stop Conditions
Given a send attempt is accepted by the app but not confirmed delivered by the provider When a transient failure occurs (e.g., 5xx, timeout, throttling) Then the system retries up to 3 times with exponential backoff delays of 15s, 30s, and 60s And retries stop immediately on success or on permanent failure (e.g., 4xx invalid recipient, hard bounce, carrier block) And duplicate deliveries of the same code are prevented across retries And each retry attempt is individually logged with outcome and next scheduled retry time
Link-Specific Code TTL and Expiration Handling
Given a verification code is issued for a specific Sponsor Gate sharelink When the code age exceeds the configured TTL (default 15 minutes) Then the code is rejected as expired and a prompt to request a new code is shown And issuing a new code invalidates any prior unredeemed codes for that link and contact And successful verification must occur within the TTL window And all expiration events are logged
Carrier Filtering Detection and Channel/Passcode Fallback
Given an SMS send returns provider reason codes indicative of carrier filtering When filtering persists after up to 2 retry attempts on alternate routes/sender IDs Then the viewer is prompted to switch to email delivery if available for the same contact And if email is unavailable or also fails, the UI offers the configured passcode option (when present) with appropriate rate limiting And the final selected fallback path and outcome are logged And no further SMS retries are attempted after fallback is offered
Diagnostics Logging and Admin Deliverability Status
Given verification activity occurs for any Sponsor Gate link When an admin views Deliverability in the Impact Board/Admin panel Then they can see per-viewer and aggregate status (Queued, Sent, Delivered, Throttled, Bounced, Filtered, Expired, Verified, Switched Channel) with timestamps And filter by date range, channel, provider, status, country, link ID, and IP/device hash And drill into an attempt to view: channel, template ID/name, provider request/response IDs, reason codes, latency, retry count, and rate-limit rule applied And export a CSV of the filtered results And sensitive identifiers are masked or hashed at rest and in exports
Audit Trail and Viewer Ledger
"As an executive director, I want a complete ledger of who accessed each shared item and when so that we can demonstrate stewardship and investigate issues if needed."
Description

Record every verification attempt and successful view with identity, method, timestamp, device/browser fingerprint (non-PII), IP geohint, and link ID. Expose a per-link ledger with filters (date range, outcome, domain) and CSV export for compliance and incident response. Support retention policies, redact-on-request, and manual revocation of a viewer’s access token with immediate invalidation. Surface a quick summary in the link details and a full audit view for admins.

Acceptance Criteria
Log All Verification Attempts and Successful Views
Given a Sponsor Gate-protected link is accessed When a viewer attempts verification (success or failure) Then an audit event is appended containing: link_id, viewer_identity (normalized), verification_method (email_code|sms_code|passcode|domain), outcome (success|fail|revoked|expired|blocked), timestamp_utc (ISO-8601), device_fingerprint_id (non-PII hash), browser_name_version, os_name_version, ip_geohint (city/region/country only), viewer_token_id (if issued), and request_id And no raw IP address or device identifiers are stored And failed attempts include failure_reason (e.g., code_mismatch, expired_code) And the event is queryable via the ledger API/UI within 5 seconds of occurrence
Per-Link Viewer Ledger with Filters and CSV Export
Given an admin or auditor opens the ledger for a specific link When they apply filters for date range, outcome, and email domain Then the results show only matching events and the total count reflects the filtered set And the ledger displays columns: timestamp_utc, viewer_identity (or [REDACTED]), domain, method, outcome, failure_reason, device_fingerprint_id, browser, os, ip_geohint, viewer_token_id, link_id, request_id And CSV export generates within 10 seconds for up to 100k rows and matches the on-screen filtered set exactly And CSV omits raw IP and includes only geohint fields And users without Admin or Auditor role cannot access export or full ledger
Retention Policy Enforcement and Scheduled Purge
Given an organization-level retention policy is set to N days When any audit event exceeds N days since timestamp_utc Then the event is purged by an automated job within 24 hours of exceeding the threshold And purged events no longer appear in the ledger UI, API, or CSV exports And changing the retention policy to a shorter window triggers purge of now-out-of-policy events within 24 hours; extending the window preserves remaining events And a purge summary (count by day) is visible to admins for compliance reporting
Redaction on Request for Specific Viewer Identity
Given an approved privacy redaction request referencing a viewer identity and scope (single link or all links in org) When an admin submits the redaction Then the system irreversibly replaces viewer_identity, device_fingerprint_id, and ip_geohint fields with [REDACTED] in all matching events within 72 hours And non-identifying fields (timestamp, method, outcome, link_id, request_id) remain intact And future CSV exports and UI views reflect the redaction And a redaction action entry is added to the admin audit log with actor, scope, and timestamp
Manual Revocation of Viewer Access Token
Given a valid viewer_token_id exists for a link When an admin revokes the token in the ledger or link details Then the token becomes invalid for all devices within 60 seconds And any subsequent access using the revoked token is blocked, prompts re-verification, and logs an audit event with outcome=blocked and reason=revoked_token And prior audit events remain unchanged; new verification issues a new viewer_token_id distinct from the revoked one
Link Details Summary and Admin Full Audit View
Given a user opens the link details page When the page loads Then a summary shows: last_viewed_at, total_verification_attempts (last 30 days), successful_views (last 30 days), failed_attempts (last 30 days), unique_viewers (last 30 days), and revocations_count And values are computed from the underlying audit events and update within 5 seconds of new events And users with Admin or Auditor role can click through to the full audit view; other roles see the summary only with export actions disabled
Tamper-Evident Audit Records and Export Integrity
Given an audit event has been created When a user attempts to modify or delete it via UI or API Then the operation is rejected (HTTP 403) because audit events are append-only except for redaction masking And only the retention job may permanently purge events based on policy And the CSV export includes a file-level SHA-256 checksum and row counts; the UI displays the checksum so admins can verify integrity after download
Sharelink Analytics and Alerts
"As an organizer, I want real-time insight into verification activity and alerts on suspicious patterns so that I can quickly adjust settings and prevent abuse."
Description

Provide analytics on gate performance: open-to-verify conversion, verification success rate by method/channel, average time to verify, resend counts, and failure reasons. Detect anomalies (e.g., many failures from one IP/domain, unusual geohints) and alert link owners via email with recommended actions (tighten policy, switch method, pause link). Offer aggregate org-level insights to optimize default policies and messaging.

Acceptance Criteria
Open-to-Verify Conversion Dashboard for Sharelink
Given a sharelink with recorded opens and completed verifications within a selected date range When the link owner loads the Sharelink Analytics view Then the system displays Open-to-Verify Conversion = completed_verifications / unique_opens rounded to one decimal place And shows the numerator and denominator values And counts unique_opens as distinct browser/device cookies per link within a 24-hour window or distinct verified identities if available, de-duplicating repeat opens within that window And completed_verifications counts each verified identity once per link within the date range And the displayed totals reconcile with the underlying event list for the same filters
Success Rate Breakdown by Method and Channel
Given a sharelink that offered one or more verification methods (SMS, Email, Passcode, Domain Allowlist) and access channels (mobile app, web) And a date range filter is applied When the owner views the Success Rate breakdown Then the system displays for each method and channel the value = completed_verifications_per_bucket / verification_attempts_per_bucket rounded to a whole percent And buckets with fewer than 20 attempts are labeled "insufficient data" and excluded from best/worst comparisons And the sum of completed_verifications across buckets equals the total completed_verifications for the date range And selecting a bucket filters the event list to that method/channel
Time-to-Verify and Resend Metrics
Given verification attempts for a sharelink within a selected date range When the owner views Time-to-Verify and Resends Then Average Time to Verify is computed as mean(seconds from first verification prompt shown to successful verification) for successful verifications in range and displayed in seconds with one decimal and sample size And the system displays Resend Count as average resends per successful verification and a distribution across 0, 1, 2, 3+ resends And metrics can be filtered by method and channel and update within 2 seconds after filter change
Failure Reason Taxonomy and Reporting
Given verification failures occur for a sharelink within a selected date range When the owner views Failure Reasons Then each failure is recorded with one standardized reason code from [invalid_code, expired_code, rate_limited, delivery_failed, domain_mismatch, geohint_mismatch, blocked_ip, user_cancelled, other] And the report displays counts and percentages per reason for the selected range And the "other" bucket is ≤ 5% of failures or the UI displays a warning badge indicating unclassified errors And clicking a reason filters the underlying failures list to that code
Anomaly Detection and Alerting with Recommendations
Given any of these anomaly rules are met for a sharelink: - ≥ 10 failures from the same IP within 5 minutes AND success rate from that IP < 10% - ≥ 5 failures from the same email domain within 10 minutes with zero successes - ≥ 3 verifications from geohints outside the org’s home country in 1 hour when the org’s baseline outside-country rate < 1% When a rule is met Then an incident is created within 1 minute and an email alert is sent to the link owner(s) within 2 minutes And the alert includes incident summary, impacted link, evidence snapshot, and one-click actions [tighten policy, switch method, pause link] And repeated triggers for the same link and rule are deduplicated for 30 minutes and aggregated into the same incident And closing the incident stops further emails while continuing to log subsequent related events
Org-Level Aggregate Insights and Policy Suggestions
Given org-level data exist for the past 30 days with ≥ 200 verification attempts When an org admin opens the Insights page Then the system displays aggregate open-to-verify conversion, success rates by method, average time to verify, average resends, and top failure reasons And the system estimates expected improvement for alternative default method order/policy using historical cohorts and only surfaces suggestions with uplift ≥ 5% and p-value < 0.05 And when a suggestion is applied, org defaults update and a confirmation appears within 5 seconds And the change is recorded with actor, timestamp (UTC), and old→new values
Alert Preferences, Suppression, and Audit Logging
Given a link owner configures alert preferences for anomaly types and delivery cadence When the owner saves preferences Then the system stores per-user preferences with org-level fallbacks And immediate alerts respect per-user preferences; hourly/daily digests are delivered at the configured cadence with correct incident counts And suppressed alerts are recorded in the audit log marked "suppressed" but not emailed And all recommendation actions (tighten policy, switch method, pause link) are audit-logged with actor, timestamp (UTC), scope (link/org), and before/after values And audit logs are exportable as CSV and retained for at least 12 months
Accessible, Localized Mobile Flow
"As a supporter on my phone, I want the verification step to be fast and accessible in my language so that I can open the link regardless of my device or abilities."
Description

Design the gate experience for fast mobile use with WCAG 2.1 AA compliance: screen reader labels, focus order, large tap targets, high-contrast options, and keyboard support. Localize all end-user strings and messages, support RTL languages, and ensure low-bandwidth performance with minimal payloads and offline-friendly error handling. Provide clear, human-readable errors and success states to reduce confusion and support across diverse volunteer and supporter devices.

Acceptance Criteria
Mobile Screen Reader and Focus Order
Given the Sponsor Gate is opened on a mobile device with VoiceOver or TalkBack enabled When the gate screen loads Then the initial focus lands on the localized screen title And the swipe/reading order matches the visual order of elements And every actionable element (inputs, buttons, links) exposes an accessible name, role, and state that matches its visible label And decorative elements are hidden from assistive tech And there are no focus traps; focus can move to and from the gate and is constrained within the modal when open And dynamic validation and status messages are announced via aria-live without requiring focus change
Tap Targets and High Contrast Mode
Given a mobile viewport width of 320 CSS px When the Sponsor Gate renders Then all tappable controls have a minimum 44x44 CSS px hit area with at least 8px spacing between adjacent targets And body text has a contrast ratio ≥ 4.5:1 and large text/icons ≥ 3:1 against background And a high-contrast option is available and discoverable within two interactions and persists for the session And the high-contrast option does not degrade functionality or overlap content on small screens
Keyboard and Switch Control Navigation
Given a user with a hardware keyboard or switch control connected to a mobile device When navigating the Sponsor Gate Then all interactive elements are reachable in a logical sequence via Tab/Shift+Tab or platform-equivalent And a visible focus indicator with contrast ≥ 3:1 is present on the focused element And Enter/Space activates buttons and toggles; Escape closes the gate when safe to do so And no keyboard traps exist; users can exit or submit without touching the screen
Full Localization and RTL Support
Given the device locale is set to a supported language When the Sponsor Gate loads Then 100% of end-user strings, form labels, placeholders, errors, and CTAs are rendered from localization files in the selected language And numbers, dates, and direction-sensitive punctuation follow the locale rules And if the locale is RTL (e.g., Arabic, Hebrew), the UI fully mirrors layout, input carets align correctly, and focus order follows RTL reading order And if a translation key is missing, content falls back to English with no placeholder tokens and is logged for remediation And users can override auto-detected language within the flow without losing state
Low-Bandwidth Load Performance and Resilience
Given a Simulated Slow 3G network (400 Kbps down, 400 Kbps up, 300 ms RTT) When first loading the Sponsor Gate Then total gate-related transfer size (HTML+CSS+JS excluding verification SMS/email payloads) is ≤ 150 KB gzipped And Largest Contentful Paint occurs ≤ 3.0 s on a mid-tier device And non-critical resources are deferred; no third-party blocking scripts are loaded before interactivity And a lightweight service worker caches shell assets for repeat visits And if the network drops during verification, an offline-friendly error is shown within 2 s with Retry and Alternate Method options; no spinner persists > 10 s And resend code is rate-limited with a visible countdown and disabled control until retry is allowed
Clear, Localized Errors and Success States
Given a user submits the Sponsor Gate with various outcomes When the code is invalid, expired, or maximum attempts are reached Then a human-readable, localized message explains the issue and next steps without exposing internal error codes And focus moves to the alert region and it is announced by screen readers via aria-live polite And links or buttons to Resend code and Use a different method are present and enabled per rate limits When verification succeeds Then a localized success state is shown for ≥ 1.5 s, and the link opens with the viewer identity recorded for watermark alignment And no PII is displayed back to the user beyond what they entered

ViewTrail

See who viewed, from where, and what was downloaded—plus get alerts on unusual activity (e.g., rapid forwards). Back-Office Stewards can audit access in seconds and revoke or tighten scopes immediately. Provides accountability that calms compliance concerns.

Requirements

Unified Access Event Capture
"As a Back-Office Steward, I want every view and download to be captured with contextual metadata so that I can audit access and investigate incidents quickly."
Description

Instrument all GiveCrew resources (e.g., documents, signup forms, shift calendars, donation receipts, Impact Board exports) to emit structured access events for views, downloads, and share/forward opens. Each event must include timestamp, actor identity (resolved contact or anonymous), resource ID/type, action, referrer, token/link ID, session ID, IP (hashed), geo, device/OS/browser, success/failure, and latency. Persist events in an append-only, immutable audit log with near–real-time ingestion (<5 seconds), indexed for low-latency querying and exportable via API. Provide SDK hooks across mobile and web clients and server-side capture for direct-download endpoints. Enforce data minimization aligned with compliance settings and respect existing permission checks.

Acceptance Criteria
Web Document View Event Captured with Required Fields
Given an authenticated steward opens a document in the GiveCrew web app When the document renders and the view event is triggered Then the emitted access event contains: timestamp (ISO-8601 UTC), actor.contact_id, resource_id, resource_type=document, action=view, referrer (nullable), token_id (nullable), session_id, ip_hash (non-reversible string), geo.city/region/country, device/os/browser, success=true, latency_ms>=0 And the event_id is globally unique And no raw IP address is transmitted or persisted
Server Direct-Download Event Capture
Given a user accesses a direct-download endpoint with a valid or invalid token When the server processes the request Then a download access event is emitted server-side with: timestamp, actor identity (resolved contact or anonymous), resource_id, resource_type, action=download, referrer (nullable), token_id, session_id (nullable), ip_hash, geo, device/os/browser (from UA), success flag, latency_ms And on authorization failure, a failure event is recorded with success=false and failure_reason=permission_denied and no sensitive resource metadata beyond resource_id/type
Near–Real-Time Ingestion and Queryability SLA
Given a test harness emits 1,000 mixed access events per minute across web, mobile, and server When events flow through the ingestion pipeline Then the 95th percentile time from client/server emit to queryable-in-index is ≤ 5 seconds And the 99th percentile is ≤ 10 seconds And end-to-end delivery success rate is ≥ 99.9% with duplicate rate ≤ 0.1%
Append-Only Immutable Audit Log
Given the audit log contains historical access events When any API or UI attempts to update or delete an existing event Then the operation is rejected and no stored records change And only append operations are allowed, producing a new immutable record And each event stores an integrity checksum, and verifying checksums over a contiguous range detects no tampering
Client SDKs and Offline/Retry Behavior
Given the Web, iOS, and Android SDKs are integrated into the app When the device is online Then events are batched and sent within 1 second or when batch size threshold is reached, whichever comes first, with an idempotency key to prevent duplicates And on transient failures, the SDK retries with exponential backoff up to a bounded maximum and guarantees at-least-once delivery When the device is offline or the app is backgrounded Then events are queued securely on-device and 99% of queued events flush within 30 seconds of reconnect, preserving in-session order and without creating duplicates server-side
Data Minimization and Permission Respect
Given compliance settings disable IP/geo collection or require coarse geolocation When access events are emitted Then ip_hash and geo fields respect settings (null when disabled, city/region/country only when coarse) And no additional PII beyond actor.contact_id is captured When a user lacks permission to access a resource Then a failure event is recorded with success=false and failure_reason=permission_denied, and no restricted resource metadata beyond resource_id/type is persisted
Indexed Query Performance and API Export
Given 10 million access events exist in the audit index When a steward queries by time range, resource_id, actor_id, and action with pagination size=100 Then the first page returns within 2 seconds p95 and results are correctly ordered by timestamp desc And the export API can stream NDJSON or CSV at ≥ 5,000 events/second with stable pagination cursors and schema headers And the API enforces scope-based authorization (audit.read required), returning 403 when missing
Identity Resolution & Link Attribution
"As a Back-Office Steward, I want views attributed to known contacts and specific share links so that I can see exactly who engaged and hold the right people accountable."
Description

Assign signed, scoped, expiring tokens to all shareable links and map them to GiveCrew contacts, roles, and campaigns. Resolve “who viewed” by combining authenticated sessions, token claims, and email/magic-link verification, with fallback to anonymous identifiers and a confidence score. Deduplicate multi-device sessions and attribute forwards by token lineage. Store minimal PII, link back to CRM profiles, and expose attribution in UI, API, and exports.

Acceptance Criteria
Scoped, Expiring Token Issuance on Shareable Links
Given a steward creates a shareable link with explicit scope and expiry, When the link is generated, Then a signed token is issued containing token_id, scope, issuer, optional subject (contact_id), campaign_id, and expiry. Given a token has passed its expiry, When any request uses the token, Then the request is rejected with 401 expired and no resource content is returned. Given a link is created without an explicit subject, When it is accessed, Then it functions and identity resolution uses runtime signals (session, token claims, email/magic-link) with anonymous fallback. Given a steward selects no-download in scope, When the link is accessed, Then download endpoints return 403 and read-only views remain accessible. Given a steward revokes the token in the dashboard, When any client attempts to use the token, Then the request is denied within 60 seconds across all devices.
Resolve Viewer Identity with Confidence Score
Given a viewer with an authenticated GiveCrew session matching a contact accesses a tokenized link, When the request is processed, Then the viewer is attributed to that contact with confidence score 1.00. Given a viewer verifies ownership via magic-link sent to email associated with a contact, When they access the link, Then the viewer is attributed to that contact with confidence score ≥ 0.90. Given no authenticated or verified signals are present, When the link is accessed, Then an anonymous identifier (anon_id) is assigned and the confidence score ≤ 0.50. Given multiple signals map to conflicting contacts, When the link is accessed, Then the highest-trust signal (authenticated session > verified email > token subject > device fingerprint) determines attribution and the confidence score reflects the trust level with recorded rationale. Given a confidence score is produced, When it is stored or displayed, Then it is a numeric value between 0.00 and 1.00 rounded to two decimals.
Session Deduplication Across Devices
Given the same viewer opens a tokenized link on multiple devices within the session window, When identity resolution ties requests to the same contact_id or anon_id and token lineage, Then the system counts one unique viewer and one active session. Given a session has no activity for 30 minutes, When new activity occurs, Then a new session is created and counted separately. Given two distinct viewers use the same forwarded link, When their verified signals differ (email/session) or device fingerprints are dissimilar, Then they are counted as separate unique viewers and sessions. Given multiple rapid requests from the same device and token within 5 seconds, When counting activity, Then they are coalesced into a single view event.
Forward Attribution via Token Lineage
Given a viewer forwards a tokenized link, When a recipient accesses it, Then the system records a child lineage_id derived from the original token and attributes the view to the child with a reference to the parent. Given split-on-forward policy is enabled, When the link is forwarded via built-in share controls or detected multi-recipient delivery, Then derivative tokens are issued with inherited scope and reduced expiry and parent→child lineage is recorded. Given a forwarded token is used after parent expiry, When access is attempted, Then the request is denied and the event is logged as expired. Given forwarded views are recorded, When the lineage stats are queried, Then the parent token shows child count and roll-up metrics matching the sum of child views.
Minimal PII Storage with CRM Linkage
Given identity resolution has completed, When persisting a view event, Then only token_id, lineage_id, contact_id (or null), role_id (or null), campaign_id, anon_id (if applicable), truncated_ip (/24 IPv4 or /48 IPv6), coarse_geo (city, country), user_agent hash, device_fingerprint hash, timestamps, and confidence_score are stored and raw email/full IP are not stored. Given magic-link verification is used, When persisting identifiers, Then only a salted hash of the email and the resolved contact_id are stored and the plaintext email is discarded after verification. Given a view event references a contact, When retrieved via UI, API, or export, Then it links to the CRM profile via contact_id without duplicating PII fields. Given the org’s retention policy for event-level identifiers is set to R days, When R days elapse, Then anon device fingerprints and truncated IPs older than R are purged by a daily job while aggregate counts remain.
Attribution Visibility in UI, API, and Exports
Given an authorized steward opens ViewTrail for a link or campaign, When data loads, Then each view row shows viewer label (contact name or Anonymous), role, campaign, timestamp, city/country, device type, confidence score, and lineage indicator. Given the API v1 client calls the viewtrail endpoint with proper scopes, When requesting data, Then the response includes the same fields as the UI and supports filtering by campaign_id, token_id, contact_id, date range, and cursor pagination. Given an export is initiated for a selected date range and campaign, When the export completes, Then the CSV contains one row per view with the fields above and totals reconcile within 1% with the UI for the same filters.
Real-Time Revocation and Scope Tightening
Given a steward revokes a token or edits its scope, When the change is saved, Then subsequent requests using that token reflect the change within 60 seconds globally. Given a client with cached privileges attempts an action removed by scope-tightening, When the request is processed, Then it is rejected with scope_mismatch and no content is returned. Given revocation occurs during an active viewing session, When the viewer makes the next request, Then a 401 revoked response is returned and the event is logged with token_id and reason.
Geo & Device Context with Privacy Controls
"As a Back-Office Steward, I want to know where accesses originate and on what devices, without over-collecting personal data, so that I can spot anomalies and stay compliant."
Description

Augment access events with city/region/country geo-IP, device type, OS, browser, and network characteristics while hashing raw IPs and honoring regional consent requirements. Flag VPN/TOR/suspicious networks, support configurable data retention and redaction, and display location/device context in the audit UI. Integrate with a vetted geo-IP provider, implement caching, and degrade gracefully when offline.

Acceptance Criteria
Enrich Access Events with Geo, Device, and Network Metadata
Given an access event with a routable IPv4/IPv6 address and a parsable User-Agent When the event is processed Then the event record includes city, region, and country from the geo-IP provider when available (else null) And the event record includes device_type, os_name_version, and browser_name_version parsed from the User-Agent And the event record includes network_asn (or isp_name when asn unavailable) and network_type in {residential, business, mobile, datacenter, unknown} And enrichment completes within 200 ms at p95 and 500 ms max per event, measured over the last 24 hours And enrichment failures set enrichment_status with a machine-readable code and do not block storing the base event
Consent-Aware Data Collection by Region
Given the access event originates from a region requiring prior consent for enrichment and consent is not present When the event is processed Then no geo-IP lookup is performed and no device or network attributes are stored And the event stores only ip_hash, timestamp, and a consent_status="blocked" flag And the audit UI displays a "Consent blocked" badge instead of location/device details Given the access event originates from a region requiring prior consent and consent is present When the event is processed Then enrichment proceeds per the geo/device/network rules and consent_status="granted" is stored
Raw IP Privacy via One-Way Hashing
Given any access event with a client IP When persisting the event Then the system stores only a salted one-way hash (SHA-256) of the raw IP as ip_hash And the raw IP is never written to databases, analytics stores, or application logs And the same raw IP yields the same ip_hash within the active salt window, and a different ip_hash after salt rotation And an automated log/database scan job runs daily and reports Fail if any plaintext IP patterns are detected
VPN/TOR/Suspicious Network Flagging and Alerts
Given an access event whose IP matches maintained VPN/proxy/TOR lists or a high-risk ASN When the event is processed Then the event is tagged with network_flags including any of ["vpn","proxy","tor","datacenter","suspicious_asn"] And a numeric risk_score (0–100) is computed and stored And events with risk_score >= 70 are highlighted in the audit UI and generate an alert to Back-Office Stewards And threat intelligence lists are refreshed at least every 24 hours and refresh outcomes are logged
Configurable Retention and Redaction
Given a retention policy (in days) for enrichment fields is configured by an admin When events exceed the configured retention window Then geo/device/network enrichment fields are automatically redacted while preserving minimal security fields (timestamp, ip_hash, event_id) And a redaction_audit record logs counts, time range, and outcome And when an admin initiates a targeted purge for a person or ip_hash Then enrichment fields are removed across primary and analytics stores within 15 minutes, with a verification report produced And failures in redaction or purge raise an operational alert within 5 minutes
Audit UI Displays Location/Device Context Clearly
Given a Back-Office Steward opens the ViewTrail audit view for a resource When a page of 50 events is rendered Then each row displays city, region, country, device_type, os, browser, and any network_flags without requiring hover And unknown or consent-blocked values render standardized badges: "Unknown", "Consent blocked", "Offline" And clicking location opens a details drawer showing provider_source, enrichment_status, timestamps, and risk_score And the page renders within 300 ms p95 and maintains WCAG 2.1 AA contrast for badges and text
Geo-IP Provider Integration, Caching, and Offline Degradation
Given the geo-IP provider is reachable and multiple events share the same IP within the TTL When enrichment is requested Then lookups are served from cache by IP hash with a default TTL of 24 hours and achieve >= 80% cache hit rate under repeated-IP load tests And outbound provider calls honor a rate limit and a 500 ms timeout per request Given the geo-IP provider is slow or unreachable When a lookup times out or fails Then the event is stored with enrichment_status set (e.g., "provider_timeout" or "provider_error") and enrichment fields null And the system falls back to a cached value if valid by TTL and queues a retry without blocking UI And operational metrics expose cache_hit_rate, provider_latency_p95/p99, and timeout_count
Unusual Activity Detection & Real-time Alerts
"As a Back-Office Steward, I want real-time alerts for unusual access patterns so that I can respond immediately before risks escalate."
Description

Provide a rule engine to detect anomalous patterns such as rapid forwards (N distinct IPs in M minutes), excessive download velocity, access from new geographies for a contact, repeated failed access attempts, and domain scope violations. Evaluate events in real time, assign severity, suppress duplicates, and send alerts via in-app notifications, email, SMS, and Slack with configurable quiet hours and escalation. Link alerts to incident views with context and remediation actions.

Acceptance Criteria
Rapid Forwards Detection and Alerting
Given a share link with rule rapid_forwards enabled and thresholds N=5 distinct IPs within M=10 minutes And a steward notification profile with in-app, email, SMS, and Slack enabled When the 5th distinct public IP accesses the link within a rolling 10-minute window Then the system flags an anomaly within 5 seconds of the triggering event And assigns severity = High per rule mapping And creates exactly one alert with deduplication key = {assetId + rule + linkId} And delivers notifications via in-app, email, SMS, and Slack to assigned recipients And includes in the alert payload: rule name, severity, triggering IPs list, access timestamps, geo lookups, contact/link owner, affected asset, and event count And provides a link to an incident view containing the full event timeline, IP/geo details, device/user agents, and recommended remediation actions (revoke link, tighten scope, block IP) And records an audit entry of detection and delivery outcomes
Excessive Download Velocity Detection
Given a rule download_velocity with threshold max_downloads=20 per 15 minutes for a contact or link And files A..Z are available on the same link When downloads for the same contact exceed 20 within 15 minutes Then an anomaly is detected in real time (under 5 seconds from the exceeding event) And severity = Medium if <=2x threshold else High if >2x threshold And one alert is created with deduplication key = {assetGroup + rule + contactId} And the alert payload lists filenames downloaded, byte totals, timestamps, client fingerprints, and originating IPs And notifications are sent via in-app, email, SMS, and Slack per recipient preferences And the alert links to an incident view with remediation actions (temporarily disable link, require re-auth, throttle downloads) And subsequent downloads within a 10-minute suppression window update the incident counter without generating a new alert
New Geography Access Anomaly
Given a rule new_geography configured with lookback=90 days and country granularity And contact C has historical access only from US and CA in the last 90 days When contact C accesses from DE (Germany) for the first time Then the system detects the anomaly within 5 seconds of access And assigns severity = Medium And generates a single alert containing previous geographies, new geography, IP, ASN, and timestamp And delivers notifications via in-app, email, SMS, and Slack per routing rules And the alert links to an incident view showing geo history, map visualization, and remediation (require step-up verification, revoke link) And a suppression window of 60 minutes prevents duplicate new_geography alerts for the same contact and country
Repeated Failed Access Attempts Lockout Alert
Given a rule failed_access with threshold=7 failures within 5 minutes for the same asset or contact And access requires a code or authenticated session When the 7th failed attempt occurs within a rolling 5-minute window Then detection occurs within 3 seconds and severity = High And the alert payload includes failure count, IPs involved, user agents, approximate geos, and targeted asset/link And notifications are sent via in-app, email, SMS, and Slack to the on-call group And the incident view provides remediation actions (temporarily block IP/ASN, invalidate codes, force password reset for account owner) And additional failures within 10 minutes increment the incident counter without new alerts unless severity escalates
Domain Scope Violation Detection
Given a share policy with allowed recipient domains = [givecrew.org, partner.org] And a rule domain_scope_violation is enabled When an access event is associated with an email identity at example.com or an unverified domain Then the system flags a violation within 5 seconds and assigns severity = Critical And creates one alert with deduplication key = {assetId + rule} And alert payload includes offending domain, asserted identity, evidence (headers/OIDC claims), and access context And notifications are sent immediately via in-app, email, SMS, and Slack with Critical priority And the incident view provides one-click remediation (revoke link, tighten scope to allowed domains, notify asset owner) And an audit log records detection, remediation actions taken, and actor identity
Duplicate Alert Suppression and Aggregation
Given suppression settings suppression_window=10 minutes and periodic_update=15 minutes And multiple identical rule violations occur for the same deduplication key When a second violation occurs within the suppression window Then no new alert objects are created; the original incident counter increments and timeline is appended And recipients do not receive duplicate channel notifications within the suppression window And after periodic_update, a single summary update is sent with aggregated counts and last-seen timestamp And if severity increases due to rule thresholds, a fresh alert is emitted bypassing suppression And all actions are captured in the incident audit trail
Quiet Hours and Escalation Routing
Given quiet hours configured 21:00–07:00 in steward timezone and escalation tiers T1=Steward, T2=On-call Lead And notification channels enabled: in-app, email, SMS, Slack When a Medium severity alert is generated at 22:00 local time Then in-app is queued, email is queued, SMS and Slack are suppressed until 07:00 And the incident shows queued status with next delivery time When a High severity alert is generated at 22:00 local time Then SMS and Slack are sent immediately to T1; in-app and email are queued And if not acknowledged within 10 minutes, escalation sends SMS and Slack to T2 And acknowledgement in any channel cancels queued deliveries and escalations And audit logs include quiet-hours decisions, escalations, acknowledgements, and delivery outcomes
Access Audit Console
"As a Back-Office Steward, I want a fast, filterable console of access activity so that I can answer compliance questions in seconds."
Description

Deliver a steward-focused dashboard to search and filter access logs by resource, contact, action, timeframe, geo, device, and token. Provide a timeline view, per-contact access history, pivot summaries (e.g., top downloaders, top resources), saved filters, and CSV/PDF exports. Ensure mobile responsiveness and performance targets of <2 seconds for common 30-day queries. Include permissions to restrict visibility by program or campaign.

Acceptance Criteria
Multi-Facet Log Search, Filter, and Timeline View (30-Day)
Given I am a Back-Office Steward with access to Program A and there are access logs within the last 30 days When I set filters: resource = "Volunteer Handbook.pdf", contact = "alex@example.org", action ∈ {view, download}, timeframe = Last 30 days, geo = "US-CA", device = "iOS", token = "share-123" Then the results table returns only entries matching all selected filters and excludes records outside Program A And the total results count equals the number of returned rows And the query completes in ≤ 2 seconds from filter apply to first render When I switch to Timeline view with the same filters Then events render in chronological order with accurate timestamps in the organization’s timezone And infinite scroll loads the next 50 events within ≤ 1 second after reaching the end
Per-Contact Access History
Given I am viewing contact "Alex Rivera" and have permission for Program A When I open the Access History tab Then I see that contact’s events scoped to my permissions, default timeframe = Last 30 days And I can filter by resource and action and the list updates within ≤ 2 seconds And clicking an event opens a details drawer with resource, action, timestamp, geo, device, and token
Pivot Summaries: Top Downloaders and Top Resources
Given I have applied timeframe = Last 30 days and program = Program A When I open Pivot Summaries Then I see "Top Downloaders" ranked by download count and "Top Resources" ranked by downloads and views And counts in each pivot match the underlying filtered logs within an exact match And clicking a pivot row applies the corresponding filter and navigates to the filtered results table
Saved Filters: Create, Apply, Update, Delete
Given I have applied a set of filters When I save the filter as "Suspected Sharing - 7 Days" with visibility = "Only me" Then the saved filter appears in my Saved Filters list and persists across sessions When I apply the saved filter later Then the filter state (all facets and view mode) is restored exactly When I update the saved filter name to "Suspected Sharing - Week" and save Then subsequent use reflects the updated definition When I delete the saved filter Then it no longer appears in the list and cannot be applied And saved filters cannot expose data outside my program/campaign permissions
CSV and PDF Exports of Filtered Results and Pivots
Given I have filtered results in table view When I export to CSV Then the file contains all rows in the current result set up to 50,000 rows with headers: event_id, timestamp (org timezone, ISO 8601), program, campaign, resource, contact, action, geo, device, token And the export completes within ≤ 10 seconds and the filename follows pattern access-logs_{org}_{yyyy-mm-dd}_{hhmmss}.csv When I export to PDF from either table or pivot view Then the PDF includes a header with org name, timeframe, applied filters, and generation timestamp, and renders the visible columns or pivot with totals And exports include only data I am permitted to view
Program/Campaign Visibility Permissions Enforcement
Given my user is restricted to Program A and Campaign X When I open the Access Audit Console without any filters Then only logs from Program A/Campaign X are displayed and filter pickers list only values within my scope When I attempt to access a log or resource outside my scope via direct URL Then I receive a 403 response and no sensitive details are revealed And all exports and pivot summaries are restricted to my scope
Mobile Responsiveness for Audit Console
Given I open the console on a mobile device (viewport width 360–430px) When I view the results table or timeline Then the layout adapts to a single-column card/table with horizontal scroll for overflow, sticky filters toggle, and tap targets ≥ 44px And all primary actions (filter, switch view, export, save filter) are reachable without horizontal scroll And applying a 30-day filter returns results within ≤ 2 seconds over a typical 4G connection (≥ 10 Mbps)
Instant Revoke & Scope Tightening
"As a Back-Office Steward, I want to revoke access and tighten sharing scopes instantly so that I can contain suspected leaks and prevent further exposure."
Description

Enable stewards to revoke individual tokens, all tokens for a resource, or all active sessions for a contact, with global propagation in under 60 seconds. Support tightening link scopes (shorter expiry, domain allowlist, geo restrictions, authentication required) and force re-authentication for sensitive resources. Log all administrative actions immutably, notify resource owners, and ensure idempotent, rollback-safe operations integrated with existing permission models.

Acceptance Criteria
Revoke Single Access Token Within 60 Seconds
Given a steward with permission and an active token T for resource R When the steward selects "Revoke Token" for T and confirms Then all requests using T are rejected with HTTP 401 within 60 seconds across all enforcement points And the token status changes to "Revoked" with a UTC timestamp And subsequent access attempts using T are recorded as "Blocked" in ViewTrail
Revoke All Tokens for a Resource Within 60 Seconds
Given resource R has multiple active access tokens When the steward selects "Revoke All Tokens" for R and confirms Then 100% of tokens for R become invalid within 60 seconds and are rejected with HTTP 401/403 And tokens for other resources remain unaffected And ViewTrail shows a single bulk-revocation event referencing all affected tokens
Revoke All Active Sessions for a Contact
Given contact C has active web and mobile sessions When the steward selects "Revoke All Sessions" for C and confirms Then C’s sessions are invalidated within 60 seconds and subsequent requests require sign-in And signed-in devices receive a sign-out event within 60 seconds And no sessions for other contacts are impacted
Tighten Link Scope: Expiry, Domain Allowlist, and Geo Restrictions
Given a share link L has expiry E and no domain or geo restrictions When the steward sets a new earlier expiry E', configures an HTTP referrer domain allowlist [example.org, partner.net], and a country allowlist [US, CA] Then access to L after time E' returns HTTP 410 within 60 seconds of configuration change And accesses to L before E' that originate from non-allowed referrers or outside allowed countries are blocked with HTTP 403 within 60 seconds And accesses from allowed referrers within allowed countries before E' continue to succeed subject to authentication requirements
Force Re-Authentication for Sensitive Resources
Given resource R is marked sensitive and has active viewers with valid sessions When the steward enables "Authentication required" and "Force re-authentication" for R Then the next access to R prompts an authentication challenge for all viewers within 60 seconds And access to R is denied until successful re-auth occurs And successful re-auth grants access without changing the viewer’s existing authorization scope
Immutable Admin Action Logging and Owner Notifications
Given audit logging and notifications are configured When any revoke or scope-tightening action is executed Then an immutable audit record is written with actor, action, target (token/resource/contact), before/after values, UTC timestamp, reason, correlation ID, and a tamper-evident hash And the record is retrievable by authorized users within 5 seconds of write And all resource owners receive a notification within 60 seconds summarizing the action with a link to the audit record, respecting notification preferences
Idempotent, Rollback-Safe Operations with Permission Enforcement
Given an idempotency key K is provided and the steward’s role grants tighten/revoke rights on the target When the same action with key K is submitted one or more times concurrently Then only one state change occurs and subsequent submissions return success with a no-op result And if any step of the operation fails, the system reverts to the pre-action state atomically and surfaces an error without partial effects And attempts to widen a link’s scope or act outside the steward’s permissions are rejected with HTTP 403 and no state change

Snapshot Posters

Freeze a moment-in-time, poster-ready PDF with embedded watermark and scope notes. Schedule automatic snapshots before key meetings so leaders have the latest numbers without live-link risk. Prints cleanly and matches the live Impact Board styling for consistency.

Requirements

Instant Snapshot Capture
"As a grassroots organizer, I want to capture a faithful PDF of the current Impact Board with one tap so that I can share numbers without exposing live data."
Description

Generate a poster-ready PDF of the current Impact Board state with one action, preserving active filters, date ranges, and segment definitions. The snapshot must render with the same typography, colors, and layout as the live Impact Board, embed an "as of" timestamp, and include pagination when content exceeds one page. The feature should be accessible on web and mobile, respect role permissions, and default to organization branding.

Acceptance Criteria
One-Tap Snapshot from Impact Board
Given an authenticated user is viewing an Impact Board with data fully loaded And active filters, date range, and segment definitions are applied When the user selects the "Create Snapshot" action Then a PDF is generated without navigating away from the board And a download/save/share prompt is displayed in the same session And the PDF filename matches "ImpactBoard_Snapshot_{orgSlug}_{YYYY-MM-DD_HH-mm-ss}_{TZ}.pdf" And an audit event "snapshot.created" is recorded with user ID, board ID, timestamp
Snapshot Preserves Filters, Date Ranges, and Segments
Given the board has filters F, date range D, and segment definitions S applied When a snapshot is created Then the PDF includes only data matching F within D and uses S for grouping/labels And all visible totals and counts in the PDF equal the live board values at capture time And no default or global filters override F, D, or S And if F, D, or S are empty, the PDF reflects the board's current default state
Styling and Branding Consistency
Given organization branding settings (logo, colors, typography) are configured or defaults exist When a snapshot is created Then PDF typography, colors, and layout match the live Impact Board within a pixel-diff threshold <= 0.5% And the org logo and brand colors appear as on the live board And interactive elements (links, tooltips, hover states) are removed or rendered inert And no UI chrome outside the board (navigation bars, buttons) is present in the PDF
As-Of Timestamp and Metadata
Given the organization timezone T and locale L are configured When a snapshot is created at time ts Then the PDF header displays "As of {localized ts}" using timezone T and locale L with timezone abbreviation And the PDF document metadata includes CreationDate=ts, Producer="GiveCrew", and Title containing the org and board names And the displayed timestamp remains constant even if the live board updates after capture
Pagination for Overflow Content
Given the board content exceeds one page at the snapshot rendering size When a snapshot is created Then all content is included without truncation or overlap across pages And page headers repeat and footers display "Page X of Y" And tables and charts break only at row/group boundaries; legends are not split from their charts And totals and subtotals remain accurate across page breaks
Role-Based Access Control
Given role permissions govern snapshot creation When an authorized user attempts to create a snapshot Then the action succeeds and a PDF is produced When an unauthorized user attempts to create a snapshot Then the control is disabled or the request is rejected with HTTP 403 and no PDF is generated And all attempts are logged with user ID, role, and outcome
Web and Mobile Parity
Given users access the Impact Board via web and mobile (iOS/Android) When a snapshot is created on each platform from the same board state Then the resulting PDFs are identical in content and styling And on web the file is downloaded to the device; on mobile the native share/save sheet is presented And the snapshot control is accessible and consistently labeled across platforms
Timezone-Aware Snapshot Scheduling
"As a team lead, I want snapshots to auto-generate before key meetings so that leaders always have the latest numbers ready."
Description

Allow users to schedule automatic snapshot generation at specific times and recurrence patterns, including offsets relative to saved meetings (e.g., 30 minutes before weekly standup). Schedules must be timezone-aware, handle daylight saving changes, avoid duplicate runs, and pause when the organization is archived. Provide a simple UI for creating, editing, and pausing schedules, and ensure snapshots inherit the saved board view and filters tied to the schedule.

Acceptance Criteria
Create schedule at local time with explicit timezone
Given a user with Manage Schedules permission and a saved Impact Board view exists When the user creates a schedule set to 09:00 in America/Los_Angeles repeating weekly on Monday and Wednesday Then the schedule stores timezone=America/Los_Angeles and local_time=09:00 and recurrence=Mon,Wed And the computed next run equals the next Monday or Wednesday at 09:00 America/Los_Angeles, converted to the correct UTC timestamp And the schedule list shows the correct timezone label and next run timestamp
Daylight saving transitions for scheduled runs
Given a daily schedule at 02:30 in Europe/Berlin When DST starts and 02:30 does not exist locally Then that day's run executes once at 03:00 local time and is recorded as the intended 02:30 occurrence Given a daily schedule at 01:30 in America/New_York on the fall-back day When 01:00–02:00 repeats Then the run executes once at the first 01:30 local time and no second run occurs for the repeated hour Given a weekly schedule at 09:00 in Australia/Sydney across a DST change When the offset to UTC changes Then the local 09:00 execution time is preserved and the UTC timestamp shifts accordingly
Duplicate run prevention and missed runs policy
Given a schedule with intended run timestamp T When workers restart, scale, or receive retry signals near T Then at most one snapshot is created for schedule_id and intended_run_at=T using an idempotency key Given the system is offline at T When service resumes after T Then the T run is skipped and the next scheduled future run is computed and displayed Given two different schedules target the same intended_run_at and view When both reach T Then each schedule produces at most one snapshot and no duplicate snapshots are created
Automatic pause when organization is archived
Given an active organization with one or more schedules When the organization is archived Then all schedules move to Paused state and no runs trigger while archived Given the organization is unarchived When the next scheduled occurrence arrives Then schedules resume from the next future occurrence without backfilling missed runs Given a schedule is manually paused When its next occurrence arrives Then no snapshot is created and the next run preview advances to the following occurrence
Offsets relative to saved meetings
Given a saved meeting series "Weekly Standup" on Mondays at 10:00 Europe/London When a schedule is set to run 30 minutes before that meeting Then the next run is Monday 09:30 Europe/London with the correct UTC conversion Given a specific meeting occurrence is rescheduled to 11:15 When the schedule recalculates Then the next run becomes 10:45 for that occurrence Given a meeting occurrence is canceled When its offset time would occur Then no snapshot is created for that canceled occurrence and the next future meeting is targeted
Snapshot inherits saved board view and filters
Given a schedule referencing saved board view ID=123 with filters Region=East and Status=Confirmed When the schedule fires Then the generated PDF snapshot renders using view ID=123 with those filters applied and matches Impact Board styling, watermark, and scope notes Given the saved board view ID=123 is updated before the run When the schedule fires Then the snapshot reflects the updated view definition and current filter values tied to the schedule Given the schedule reference is changed to view ID=456 When the next run occurs Then the snapshot uses view ID=456
Schedule management UI: create, edit, pause, and preview
Given the schedule form with fields [name, recurrence, local time, timezone, meeting offset (optional), target view, active toggle] When a user enters valid inputs Then Save becomes enabled and on save the schedule persists and appears in the list Given a saved schedule When edited (e.g., timezone changed or recurrence updated) Then the next three upcoming run times update immediately and are correct for the new settings Given a saved schedule When toggled to Paused Then its status displays Paused and no run triggers until reactivated
Embedded Watermark & Scope Notes
"As a director, I want clear watermarks and scope notes on snapshots so that readers understand the data context and avoid mistaking it for a live dashboard."
Description

Embed a configurable watermark on every page of the PDF indicating "Snapshot – Not Live" and the capture timestamp, and include a scope notes block summarizing filters, date ranges, and data sources. Watermark opacity and position must be adjustable to remain visible without impairing readability, and scope notes must be standardized and placed consistently for print clarity.

Acceptance Criteria
Watermark displays correct label and timestamp on every PDF page
Given a Snapshot Poster PDF is generated at capture time T_capture in organization timezone TZ When the PDF is opened in a standards-compliant PDF viewer Then every page displays a watermark containing the exact text "Snapshot – Not Live" And the watermark includes the timestamp formatted "YYYY-MM-DD HH:mm TZ" matching T_capture within ±1 second And the watermark is embedded into page content (not a toggleable annotation/form layer) And no foreground text contrast ratio falls below 4.5:1 as a result of the watermark overlay
Configurable watermark opacity and position apply correctly
Given a user configures watermark opacity O in the inclusive range 5%–25% and selects position P from {diagonal-center, top-left, top-right, bottom-left, bottom-right} When the snapshot PDF is generated Then the rendered watermark opacity equals O within ±1% (or defaults to 12% if unset) And the watermark appears at position P with minimum 12pt padding from page margins and key content areas (titles, legends, totals) And if O or P are outside allowed values, the settings form prevents save and displays an inline validation message; generation does not proceed until corrected
Standardized scope notes content accurately reflects snapshot filters, date range, and sources
Given filters F, date range D, and data sources S are applied to the Impact Board at capture time T_capture When the snapshot PDF is generated Then the scope notes block contains the labeled sections in this fixed order: "Filters", "Date range", "Data sources", "Captured at" And the values exactly reflect F, D, S, and T_capture at the moment of capture And empty values render as "None" and no sections are omitted And the timestamp format matches the watermark format "YYYY-MM-DD HH:mm TZ"
Consistent scope notes placement with no content overlap
Given a single- or multi-page Snapshot Poster for Letter and A4 paper sizes When the PDF is generated Then the scope notes block appears on page 1 only, aligned bottom-left, occupying no more than 20% of page height and 40% of page width And the block uses a consistent bounding box relative to page margins (minimum margin 0.5 in) across all pages and paper sizes And no charts, tables, or text overlap or are obscured by the scope notes block And subsequent pages reserve equivalent footer space to maintain consistent layout flow
Print-ready output matches Impact Board styling without readability loss
Given the PDF is printed at 100% scale on standard office printers or exported via system print dialogs When the document is printed or previewed Then all body text (≥10pt) and chart labels remain fully legible with no clipping or pixelation And the scope notes and global typography (typefaces, weights, sizes) and color palette match the live Impact Board design tokens within ±0.5pt for sizes and exact hex color matches And vector text and shapes remain vector in the PDF (no unintended rasterization), and the watermark renders without banding
Graceful handling of missing or invalid watermark/scope settings
Given a user attempts to save or generate with missing or invalid watermark or scope notes configuration When the user saves settings or generates a snapshot Then required fields are validated inline with specific error messages and guidance And default settings apply when optional fields are unset (opacity=12%, position=diagonal-center; scope notes placement=page 1 bottom-left) And generation is blocked only when required data (capture timestamp) is unavailable, returning a clear error with retry guidance; no PDF is produced in that case
Print-Optimized PDF Output
"As a volunteer coordinator, I want snapshots that print cleanly on common printers so that I can post progress posters onsite without reformatting."
Description

Produce high-fidelity PDFs optimized for office and copy-shop printing, supporting Letter and A4 sizes, 300 DPI assets, embedded fonts, CMYK-safe palettes, and margin-safe layout. Provide options for color and grayscale, automatic page breaks, bleed and crop marks toggles, and ensure QR codes and small text remain legible when printed.

Acceptance Criteria
Letter and A4 Page Size Selection
Given a user selects "Letter" in the Snapshot Posters export dialog When the PDF is generated Then the PDF page size is exactly 612 x 792 pt (8.5 x 11 in) And printing at 100% scale on a standard office printer yields no clipping within a 0.25 in (6.35 mm) safe area Given a user selects "A4" in the Snapshot Posters export dialog When the PDF is generated Then the PDF page size is exactly 595 x 842 pt (210 x 297 mm) And printing at 100% scale on a standard office printer yields no clipping within a 6.35 mm (0.25 in) safe area
300 PPI Assets and Embedded Fonts
Given the poster contains raster images, logos, and icons When the PDF is generated Then the effective resolution of all raster assets at final placement size is >= 300 PPI And no raster asset is upscaled above its native resolution And all fonts used are embedded (subset or full) with no missing font warnings in Acrobat Preflight And the PDF opens without font substitution on a system where the fonts are not installed
CMYK-Safe Color and Grayscale Modes
Given the user selects "Color (CMYK)" When the PDF is generated Then all objects are CMYK only; no RGB or spot colors remain And an OutputIntent ICC profile is embedded And total ink coverage does not exceed 280% as reported by Acrobat Preflight And body text black is 100K (0C,0M,0Y,100K), not rich black Given the user selects "Grayscale" When the PDF is generated Then all objects are DeviceGray only; no CMYK or RGB objects remain And black text remains 100% K
Margin-Safe Layout and Automatic Page Breaks
Given the content exceeds one page When the PDF is generated Then automatic page breaks occur only between content blocks (no block is split) And headers, footers, and QR codes are not split across pages And no element overlaps or is truncated at a page boundary And a minimum inner margin of 0.25 in (6.35 mm) is maintained on all sides on every page
Bleed and Crop Marks Toggles
Given bleed and crop marks are toggled ON with a bleed value of 3 mm When the PDF is generated Then the PDF includes 3 mm bleed on all sides and standard crop marks offset >= 2 mm from the trim And the trim size matches the selected page size (Letter or A4) Given bleed and crop marks are toggled OFF When the PDF is generated Then the PDF contains no crop/registration marks and no bleed area beyond the trim
QR Code and Small Text Print Legibility
Given the poster includes at least one QR code and text sized between 8–10 pt When the PDF is printed on a 600 dpi office laser printer at 100% scale on Letter and A4 Then each QR code is scannable using iOS Camera and Google Lens from 12 in (30 cm) on 3 separate prints And each QR code has module size >= 0.3 mm and ECC level M or higher And minimum text size is >= 8 pt for captions and >= 9 pt for body, with minimum stroke weight >= 0.25 pt And no hairlines drop out or fill-in defects are observed at normal viewing distance
Styling Consistency with Live Impact Board
Given a live Impact Board using an approved theme When a Snapshot Poster PDF is generated for the same data state Then typography (font families, weights, sizes) and spacing tokens match the theme spec And color tokens are mapped to CMYK values per the CMYK-safe palette And an automated 300 DPI visual diff of rasterized PDF vs a PNG export of the live board shows <= 2% pixel difference excluding watermark/scope areas And watermark and scope notes are included and do not intrude into the 0.25 in (6.35 mm) safe area
Secure Static Distribution
"As an operations manager, I want to share snapshots securely with stakeholders so that they can view the latest numbers without needing access to the live system."
Description

Enable distribution of snapshots as static files via email, secure expiring links, and optional Slack/Teams uploads, without exposing live dashboard links. Links must support expiration, download limits, and access checks based on organization membership, and emails should include inline preview and metadata such as "as of" time and schedule name.

Acceptance Criteria
Email Snapshot with Inline Preview and Metadata
Given a completed snapshot poster exists with a schedule name and an as-of timestamp And the sender selects Email distribution to one or more recipients When the email is generated and sent Then the email body contains an inline preview image of the snapshot’s first page And the body includes the as-of timestamp and the schedule name And the static PDF snapshot is attached with a filename that includes the schedule name and as-of date And no live dashboard URLs are present in the email body or attachment metadata And the PDF content matches the snapshot at the as-of time
Expiring Link with Download Limit Enforcement
Given an owner creates a secure link to a snapshot with an expiration timestamp and a download limit of N When a recipient accesses the link before it expires and while remaining downloads > 0 Then the static file is downloadable and the remaining download count is decremented by 1 And repeated access is allowed until the count reaches 0 When the link is accessed after expiration or after the download limit is reached Then the download is blocked and the user sees an expiration or limit-reached message And no file data is streamed on blocked attempts
Organization Membership Access Check for Links
Given a secure link is configured to require membership in Organization X When an authenticated user who is an active member of Organization X opens the link Then the file download is permitted When an authenticated user who is not a member or whose membership is inactive opens the link Then access is denied with an explanation and the download limit is not decremented When an unauthenticated user opens the link Then they are prompted to authenticate and are denied until authenticated as an active member
Slack/Teams Upload Posts Static PDF Without Live Link
Given a completed snapshot and valid Slack or Microsoft Teams integration When the user selects Upload to Slack or Upload to Teams for a chosen channel Then the service posts the static PDF file into the selected channel And the message text includes the as-of timestamp and schedule name And the message contains no live dashboard URLs And channel members can download the PDF directly from the message
No Live Dashboard Exposure in Distributed Artifacts
Given a snapshot is distributed via email, secure link, or Slack/Teams When a recipient inspects the email content, message content, PDF, and any link landing page Then no URL pointing to the live Impact Board or live dashboard is present And all hyperlinks inside the PDF that would navigate to live content are removed or disabled And the PDF renders fully offline without requiring network access
Configure and Enforce Link Constraints
Given an admin is creating or editing a secure link for a snapshot When they set the expiration timestamp, download limit, and membership requirement Then the system saves those settings And the settings are returned by the UI or API for verification And subsequent link accesses enforce the saved expiration, download limit, and membership requirement
Snapshot Versioning & Audit Trail
"As an admin, I want a versioned history of snapshots so that I can verify what was shared and when for audits."
Description

Store each snapshot as an immutable version with metadata including creator, schedule source, capture timestamp, applied filters, data hash, and file size. Provide a browsable history with search and retention policies, and log access and distribution events to support compliance and troubleshooting.

Acceptance Criteria
Immutable Snapshot Version Save
Given a user or schedule triggers a snapshot When the snapshot capture completes Then the system stores a read-only, immutable version with a unique version_id And metadata includes creator_id (or "system" for scheduled), schedule_source (manual|schedule_id), capture_timestamp_utc, org_timezone, applied_filters (JSON), data_hash_sha256, and file_size_bytes And any attempt to modify or overwrite the snapshot file or metadata via UI or API is blocked with HTTP 403 and error code SNAPSHOT_IMMUTABLE And recomputing the SHA-256 of the downloaded file matches data_hash_sha256 And the version appears in history within 5 seconds of capture
History Browsing and Search
Given a user with Reports.View permission When they open Snapshot History Then the list shows version_id, local capture timestamp, creator, schedule_source, file_size, and a summary of applied_filters And they can filter by date range, creator, schedule_source, text match on filters/scope notes, and data_hash prefix (>=6 chars) And results are sortable by capture timestamp and creator and are paginated (25 per page by default) And for up to 5,000 snapshots, initial load is <= 2 seconds and filtered queries are <= 3 seconds at p95
Retention Policy Enforcement
Given an admin configures a snapshot retention policy in days When a snapshot exceeds the retention period and is not on legal hold Then an automated job purges it daily at 02:00 in the org's local timezone And purge removes the file and metadata and appends an immutable PURGE audit event with version_id and purge_reason=retention And admins can place or remove legal holds on specific snapshots; held snapshots are excluded from purge; hold actions are audited And changes to the retention policy (enable/disable/value) are audited with actor, old_value, new_value, and timestamp And a soft-deleted state is visible to admins for 30 days with link to the PURGE event; after 30 days only the purge event remains
Access and Distribution Event Logging
Given a user or service accesses or distributes a snapshot When actions occur (view, download, share_email, share_link, export_history, api_get_file) Then an audit event is recorded with event_id, actor_id (or service token), timestamp_utc, action, target_version_id, ip, user_agent, channel (UI|API), success/failure, and http_status And share events record recipient (email or URL) and expiration where applicable And denied or failed access attempts are logged with reason (unauthorized|forbidden|not_found) And audit events are write-once append-only; any attempt to alter or delete them is blocked with HTTP 403 And admins with Audit.View can filter audit events by date range, actor, action, and target_version_id and export CSV; p95 query latency is <= 3 seconds for up to 100,000 events
Audit Trail Export and Integrity Verification
Given an admin requests an audit trail export for a date range When the export completes Then CSV and JSON files are generated with a SHA-256 checksum and detached signature using the system signing key And the export includes generated_at_utc, org_id, row_count, and a hash of the raw dataset in the header/manifest And an EXPORT audit event is appended with parameters and a link to the export artifact And an integrity endpoint allows re-hashing a stored snapshot file and comparing with data_hash_sha256; any mismatch triggers an INTEGRITY_FAIL audit event and admin alert
Scheduled Snapshot Attribution and Timing
Given a snapshot is produced by a configured schedule run at time T When the schedule fires Then the resulting snapshot metadata records schedule_source=schedule_id and trigger_timestamp_utc And capture_timestamp_utc is within ±2 minutes of T; deviations > 2 minutes create a SCHEDULE_DRIFT audit event with reason And on failure the system retries up to 3 times with exponential backoff; each failure appends a SCHEDULE_FAIL event with error details And manual snapshots taken within 10 minutes of a scheduled run are labeled schedule_source=manual and are stored distinctly (no merge)
Consistent Data Freeze & Async Generation
"As a product user, I want snapshots to reflect a consistent point in time and complete reliably in the background so that I can trust the numbers and keep working."
Description

Guarantee read-consistent data across donors, signups, and shifts by freezing a consistent view during capture using transactional reads or snapshot isolation. Generate PDFs asynchronously in a background queue with progress status, retries, and alerting on failure, ensuring the UI remains responsive and heavy loads are handled predictably.

Acceptance Criteria
Consistent Cross-Entity Snapshot at Request Time
Given donors, signups, and shifts are being updated concurrently And a user requests a Snapshot Poster at 10:00:00 When the snapshot is initiated Then all reads for donors, signups, and shifts occur against a single immutable snapshot taken at 10:00:00 ± 1s And totals, counts, and aggregates in the PDF reflect only data committed at or before the snapshot time And updates committed after the snapshot time do not appear in the PDF And the snapshot timestamp and version identifier are recorded in job metadata and embedded in the PDF footer
Single Snapshot Used End-to-End in PDF Generation
Given a snapshot_version_id is acquired at start of job When the generator queries each section (donations, volunteer hours, shifts filled) Then every query is executed with the same snapshot_version_id And a hash of each dataset (pre-render) is computed and logged And the hash remains constant across retries within the same job And the PDF metadata includes snapshot_version_id to verify end-to-end consistency
Async Generation with Non-Blocking UI and Progress
Given a user triggers snapshot generation from the Impact Board When the job is enqueued Then the UI immediately returns control within 300 ms p95 And the job status is visible as queued > processing > completed or failed And progress updates are emitted at least every 2 seconds while processing And for datasets ≤ 10,000 records, the job completes within 20 seconds p95 and 60 seconds p99 And the user can navigate away and later retrieve the completed PDF from History without data loss
Retry with Exponential Backoff and Idempotency
Given transient errors (HTTP 5xx, network timeout, rate limit) occur during generation When the job fails a step Then it is retried up to 3 times with exponential backoff of 2s, 10s, and 30s (cap 5 minutes total) And retries reuse the original snapshot_version_id and input parameters And duplicate job submissions with the same idempotency key within 10 minutes result in a single PDF artifact And no duplicate receipts, notifications, or files are created
Failure Alerting with Safe Diagnostics
Given a job exhausts all retries and fails When the final failure is recorded Then an alert is sent to the configured channel within 60 seconds containing job_id, org_id, snapshot_version_id, error_class, and correlation_id And no donor PII or raw record contents are included in the alert And the job status is set to failed with a user-visible summary and a retry action available to admins And structured logs include stack trace, timing, and resource utilization for postmortem
Scheduled Snapshots Trigger Consistent Freezes
Given a snapshot is scheduled for 15 minutes before a meeting at 11:00 When the scheduler triggers the job at 10:45:00 Then the snapshot is taken at 10:45:00 ± 30s and used for the entire generation And the job enters the queue within 5 seconds of trigger And if the queue delay exceeds 60 seconds, the system records delay_reason and still uses the original snapshot_version_id And the completed PDF is available before 10:50:00 p95
Predictable Behavior Under Heavy Load
Given 200 concurrent snapshot requests across 50 organizations within 5 minutes When jobs are enqueued and executed Then database read latency for snapshot queries remains under 150 ms p95 and 300 ms p99 And active DB connections stay below 80% of the configured limit with zero deadlocks observed And queue wait time stays under 60 seconds p95 and 120 seconds p99 And job completion stays under 5 minutes p95 and 8 minutes p99 And no job is dropped; backpressure applies by pausing new starts when error rate > 5% over 1 minute

SmartRotate

Adaptive QR rotation that speeds up or slows down based on scan volume and crowd density. Each code auto-expires, blocks screenshot re-use, and shows a big on-screen countdown so lines keep moving. One tap toggles between Signup and Give flows, and a printable fallback sheet with short-lived codes covers true dead zones.

Requirements

Adaptive QR Rotation
"As a field organizer, I want QR codes to rotate faster when lines grow so that signups and donations keep moving without bottlenecks."
Description

A dynamic rotation mechanism that adjusts QR code refresh intervals based on real-time scan throughput and organizer-set crowd density. It monitors scans per minute, applies smoothing to avoid jitter, and enforces configurable min/max bounds (e.g., 10–60 seconds). The countdown timer is synchronized to token TTL and displayed prominently to guide user behavior. Integrates with GiveCrew kiosk mode and admin console to persist per-event settings and campaign attribution. Includes pause/resume, floor/ceiling guardrails, and auto-recovery after backgrounding or app interruptions to maintain flow continuity.

Acceptance Criteria
Adaptive Interval Scaling with Smoothing and Bounds
Given per-event settings min_interval=10s, max_interval=60s and smoothing_threshold=20% and current_interval=40s When measured scans_per_minute increases by at least 20% and remains elevated for 30s Then the next rotation interval is strictly less than 40s and not less than 10s and interval updates no more than once per rotation And when measured scans_per_minute decreases by at least 20% and remains lower for 30s Then the next rotation interval is strictly greater than the last and not greater than 60s
Countdown Synchronization to Token TTL
Given token_TTL equals the current rotation interval When a new QR is rendered in kiosk mode Then a visible countdown in seconds is displayed and decrements in real time with drift <= 250ms per 30s And when the countdown reaches 0, the QR is replaced within 500ms and the prior token is rejected as expired
Token Expiry and Screenshot Reuse Prevention
Given a QR payload that includes a short-lived token with TTL=30s When the token is scanned after its TTL has elapsed Then the server responds with an expired state and does not start Signup or Give flows And consecutive rotations produce non-identical QR payloads And scanning a screenshot of a prior rotation displays an "expired, please rescan live code" message
Pause/Resume and Auto-Recovery After Interruptions
Given rotation is active with interval=30s When the operator taps Pause Then the countdown halts and the current QR remains valid and rotation does not advance while paused When the operator taps Resume Then the countdown resumes from the paused remaining time and rotation continues When the app is backgrounded or interrupted Then upon returning to foreground a valid, non-expired QR is shown within 1s and the countdown reflects the actual remaining TTL or a fresh token if the prior expired
Settings Persistence and Campaign Attribution Integration
Given per-event rotation settings are saved in the admin console (min, max, smoothing, crowd_density, campaign_tag) When kiosk mode is launched for that event Then those settings are loaded and applied before the first QR renders and cached locally for offline continuity When settings are changed in the admin console while kiosk is online Then kiosk applies the new settings within one rotation cycle And all scan events emitted from that kiosk include the campaign_tag for attribution
Guardrails Configuration and Validation
Given a user configures min and max rotation bounds in the admin console When min is set greater than max or outside allowed range [5s, 120s] Then validation prevents saving and shows an error with the permitted range When valid values are saved (e.g., min=10s, max=60s) Then the kiosk never emits rotation intervals below 10s or above 60s
Secure QR Tokens & Anti-Replay
"As a donor, I want each QR to be valid only briefly so that screenshots can’t be reused to spoof transactions."
Description

Server-signed, short-lived QR payloads that auto-expire and prevent screenshot reuse. Each QR encodes a nonce, flow type (Signup/Give), campaign context, and expiry timestamp, signed with rotating keys. Backend validation checks signature, TTL, and replay status, marks tokens as used, and redirects to the appropriate form. Supports configurable TTLs and batch prefetch for limited connectivity, with reconciliation and revocation on reconnect. No PII is embedded in the token, aligning with GiveCrew privacy and minimizing risk.

Acceptance Criteria
Server Signature Verification & Key Rotation
Given a QR token signed with the current active key When the backend validates the token Then the signature verification succeeds and processing continues Given a QR token signed with the immediately previous key within the configured rotation overlap window When the backend validates the token Then the signature verification succeeds Given a QR token signed with an unknown or revoked key When the backend validates the token Then the request is rejected with HTTP 401 and error code SIGNATURE_INVALID, and no side effects are recorded Given a valid token When server time has up to ±60 seconds skew from the issuer time Then signature verification allows the configured leeway and does not incorrectly reject the token
TTL Enforcement & Expiry Handling
Given a campaign TTL of 30 seconds When a token is presented at t=31s after issuance Then the backend rejects it with HTTP 410 and error code TOKEN_EXPIRED and does not mark it as used Given a token within its TTL When validated Then it is accepted and processing continues Given an expired token When presented multiple times Then all attempts are rejected with TOKEN_EXPIRED Given a valid token When accepted Then the response includes the server-evaluated expiry timestamp to allow clients to render countdowns
Anti-Replay via Nonce Tracking
Given a valid unused token nonce When first submitted Then the backend marks the nonce as used atomically and issues a 302 redirect to the appropriate flow Given the same token nonce When submitted again (even within TTL) Then the backend rejects it with HTTP 409 and error code TOKEN_REPLAYED Given horizontal scaling When concurrent requests attempt to validate the same nonce Then exactly one succeeds and the others receive TOKEN_REPLAYED, as verified by idempotent datastore writes Given a used nonce When TTL expires Then subsequent submissions still return TOKEN_REPLAYED (not TOKEN_EXPIRED), preserving accurate cause
Flow Routing and Campaign Scoping
Given a token with flowType=Signup and campaignId=ABC When validated Then the user is redirected to /signup with campaign context ABC and the validation audit log records flowType=Signup and campaignId=ABC Given a token with flowType=Give and campaignId=ABC When validated Then the user is redirected to /give with campaign context ABC Given a token with an unknown flowType When validated Then the backend returns HTTP 400 with error code INVALID_FLOW Given a token with a nonexistent or inactive campaignId When validated Then the backend returns HTTP 404 with error code CAMPAIGN_NOT_FOUND
Configurable TTLs per Flow/Campaign
Given campaign X has TTL=20s and campaign Y has TTL=60s When tokens are issued for each Then their embedded expiry timestamps reflect the configured TTLs Given flowType=Signup configured with TTL=45s and flowType=Give with TTL=30s within the same campaign When tokens are issued Then the TTL per token matches the corresponding flow configuration Given TTL is updated in configuration When new tokens are issued after the change Then they use the new TTL, and previously issued tokens retain their original expiry Given an invalid TTL configuration (e.g., <5s or >5m) When saved Then the system rejects it with validation error INVALID_TTL_RANGE
Offline Batch Prefetch, Reconciliation, and Revocation
Given a device prefetches 50 tokens for campaign ABC with TTL=30s When connectivity drops Then the device can continue rotating and displaying tokens from the prefetched batch without requesting new ones Given prefetched tokens are scanned while offline When the backend later receives reconciliation messages on reconnect Then it marks the corresponding nonces as used with the original timestamps and denies any subsequent replay attempts Given the operator issues a revoke command during reconnect When processed Then all unused prefetched tokens for that batch are invalidated server-side within 5 seconds and clients discard them Given a prefetched batch stored locally When inspected at rest Then tokens are encrypted using the app’s key, and no plaintext tokens are accessible
Privacy — No PII in Token or Logs
Given any issued token When base64-decoded and parsed Then it contains only nonce, flowType, campaignId, and exp, and no fields matching PII (name, email, phone, address, payment info) Given server request logs for token validation When reviewed Then they contain only token hash/fingerprint and metadata (flowType, campaignId) and never include raw token payload or PII Given a static analyzer/test harness When run against the token issuer Then any attempt to inject PII fields into the payload is blocked with error code PII_NOT_ALLOWED
One-Tap Flow Toggle
"As a volunteer lead, I want to switch between signup and donation QR codes with one tap so that I can adapt to the crowd without reconfiguring the app."
Description

A single control that switches the active QR between Signup and Give flows without leaving the SmartRotate screen. The toggle updates the encoded destination, tracking parameters, labels, and color accents, and persists the selection per event. Changes take effect at the next rotation boundary to avoid mid-scan race conditions. Enforces role-based permissions, logs toggle events for analytics, and provides visual confirmation to prevent operator error.

Acceptance Criteria
Update Encoded Destination, Tracking, Labels, and Color on Toggle
Given an authenticated Organizer on the SmartRotate screen for event E with current flow = Signup and rotation countdown T>0 When the operator taps the one-tap toggle to Give Then the current QR continues to encode the Signup destination until T=0 And a "Pending switch to Give at rotate" indicator appears within 200 ms of the tap And at the next rotation boundary (T=0), the rendered QR updates to encode the Give destination URL with parameters: flow=give, event_id=E, rot_seq = previous rot_seq + 1 And the on-screen flow label updates to "Give" and the accent color token equals flow_palette[give].accent within 300 ms of the boundary
Enforce Role-Based Permissions on Toggle
Given a user with role = Volunteer viewing SmartRotate for event E When they attempt to toggle flows Then the toggle does not change state, a tooltip "Insufficient permission" and a toast "Only Organizers and Captains can switch flows" appear within 300 ms, and a denied audit record is written Given a user with role = Organizer or Captain When they toggle flows Then the toggle action is accepted and scheduled for the next rotation boundary
Persist Flow Selection Per Event Across Sessions
Given event E has last saved flow selection = Give at time t0 When any authorized user opens SmartRotate for event E on any device Then the toggle default state = Give before the first QR render When an authorized user switches the flow to Signup Then the selection is persisted to the backend within 1 second of the tap and is reflected when reopening SmartRotate for event E on the same or another device And if the device is offline, the selection is queued locally and syncs within 10 seconds of reconnect, after which other devices show the updated flow
Apply Toggle at Next Rotation Boundary Without Disrupting Scans
Given active scans are occurring (≥1 scan/5s) and countdown T>0 When the operator toggles from Signup to Give Then no QR image redraw occurs before T=0 And there is exactly one QR update at T=0 (±100 ms) And scans with server receive_time < boundary_ts are attributed to Signup; scans ≥ boundary_ts are attributed to Give And there is zero overlap window where both flows are accepted for the same QR code value
Log Toggle Events for Analytics
Given an authorized user toggles from F1 to F2 on event E from device D Then the system creates an analytics log entry with fields: event_id, actor_id, device_id, from_flow=F1, to_flow=F2, toggle_ts (server UTC), boundary_seq, outcome=success, reason=null And for unauthorized attempts, outcome=failure and reason=permission_denied And the log is queryable via the Admin Logs API within 5 minutes of the action
Provide Immediate Visual Confirmation to Operator
Given an operator taps the toggle to change flows Then a confirmation toast "Will switch to [Flow] at rotation" with a check icon appears within 200 ms and auto-dismisses after 2 seconds And the toggle control visually snaps to the target flow state with an animated thumb transition of ≤150 ms And a header badge shows "Switching to [Flow]" with the target accent color until the boundary, then changes to "[Flow]" at the boundary
Countdown Kiosk UI
"As a kiosk operator, I want a big, clear countdown on the QR screen so that people see when to scan and the line keeps moving."
Description

A full-screen, high-contrast display with a large QR code, prominent numeric countdown and progress ring, and optional haptic/sound cues at rotation to guide scanning cadence. Supports dark/light themes, brightness boost, screen lock, and WCAG AA contrast for accessibility. Shows flow badge (Signup/Give), campaign name, and localized labels. Prevents accidental navigation, offers an operator quick menu with PIN guard, and degrades gracefully on smaller devices to ensure clarity in all environments.

Acceptance Criteria
Full-Screen QR with Countdown and Cues
Given the kiosk is in active session When the display loads Then the app renders in true full-screen with no browser UI, OS status bar, or navigation chrome visible And the QR code size is at least 60% of the shorter screen dimension and no less than 280 px And a numeric countdown is clearly visible with text height >= 48 px or 12% of screen height (whichever is greater) And a circular progress ring encircles or accompanies the QR and completes a full sweep in sync with the countdown And the countdown updates at >= 10 Hz and never lags the rotation timer by more than 250 ms And on rotation, optional haptic (50–100 ms) and a 0.3–0.6 s chime play when enabled, respecting device mute/DND
Accessibility and WCAG AA Compliance
Given any theme, campaign color, or branding When contrast is measured per WCAG 2.2 AA Then normal text has contrast ratio >= 4.5:1 and large text (>= 18 pt or 14 pt bold) >= 3:1 And essential non-text UI (QR border, progress ring, badges, buttons) has contrast >= 3:1 against adjacent colors And all visible labels and buttons have programmatic names matching localized copy And keyboard/focus order is constrained to the operator quick menu when opened and otherwise presents no focusable distractions And automated accessibility audit reports score >= 90
Theme Switching and Brightness Boost
Given the operator opens the quick menu When Dark or Light theme is toggled Then the UI applies the theme within 300 ms and persists the preference for the session And all elements retain WCAG AA contrast after the switch When Brightness Boost is enabled Then screen brightness is set to >= 90% on supported OS within 200 ms and the screen is kept awake And on unsupported OS a persistent in-app prompt instructs manual brightness change until dismissed by the operator
Navigation Lock, Orientation Lock, and Screen Wake
Given kiosk mode is active When a user taps, swipes, or invokes system back/edge gestures Then no navigation away from the kiosk view occurs and no external links open And the screen does not dim or sleep during an active session (verified for at least 8 hours idle) And the UI enters immersive/edge-gesture suppression on supported devices And the display orientation remains locked to the current orientation unless changed by the operator
Operator Quick Menu with PIN Guard
Given kiosk mode is active When the operator performs a 3-second long-press on the top-right 64x64 px hotspot or triple-taps within 1.5 s Then the operator menu appears within 300 ms and blurs/dims the background And entry requires a 4–8 digit PIN with masked input and paste disabled And after 5 failed attempts the menu locks for 60 s and the attempt is timestamp-logged And inactivity for 10 s auto-dismisses the menu And the menu provides controls for theme, brightness boost, haptic/sound cues, and flow selection (Signup/Give)
Flow Badge, Campaign Name, and Localization
Given a campaign context is loaded When the current flow is Signup or Give Then a flow badge labeled "Signup" or "Give" is visible with distinct colors meeting contrast requirements And the campaign name is visible; if it exceeds available width it truncates with ellipsis after one line And all static labels and countdown numerals render in the device locale within 200 ms of locale change And if a translation key is missing, English fallback displays and the missing key is logged And numerals use locale-appropriate digit shapes and separators where applicable
Responsive Degradation on Small Devices
Given a device with viewport width < 360 px or height < 640 px When the kiosk UI renders Then the QR minimum dimension is maintained at >= 220 px before any other element is hidden And the countdown switches to a stacked layout with numeric size >= 40 px And all interactive targets (operator hotspot, toggles) are >= 44x44 px And animation frame rate remains >= 50 FPS during countdown/progress animation on mid-tier devices And peak memory usage of the kiosk view does not exceed 200 MB during an active session
Printable Dead-Zone Codes
"As an organizer in a dead-zone venue, I want a printable sheet of rotating short-lived codes so that people can still sign up or give without a live screen."
Description

A generator that produces printable sheets of time-bucketed, short-lived QR codes for venues without reliable power or display connectivity. Admins select date/time windows and flow; the system outputs a branded PDF with sequential codes (e.g., 60 codes, each valid for 1 minute) labeled with minute markers and usage instructions. Codes embed signed expiries to prevent reuse and route to lightweight forms that can queue submissions when connectivity resumes. Includes batch inventory tracking, watermarking, and the ability to revoke remaining codes if a sheet is compromised.

Acceptance Criteria
Generate Branded Time-Bucketed PDF Sheet
Given I am an admin with org branding configured and select Flow = Signup or Give, a start date/time, end date/time, time zone, and bucket size = 1 minute When I click Generate for a window of up to 60 minutes Then a PDF is produced in <= 10 seconds containing sequential QR codes equal to total buckets, each cell labeled with the exact minute marker, flow label, and brief usage instructions And each QR is at least 25 mm wide with ECC level M or higher and prints legibly at 300 DPI And the PDF footer shows organization name, batch ID, time zone abbreviation, page X of Y, and generation timestamp
Signed Expiry Validation and Anti-Reuse
Given a code’s token embeds a signed expiry and batch ID When the code is scanned before its expiry minute elapses Then the backend validates the signature and routes to the selected flow form When the same code is scanned after expiry Then the user sees an Expired Code page and the request is rejected with HTTP 410 When the token is tampered or forged Then the request is rejected with HTTP 400 and no form is served And tokens include at least 128 bits of entropy and are not predictable across sheets
Lightweight Form with Offline Queue and Auto-Resume
Given a valid scan resolves to the lightweight flow form Then initial page load is <= 100 KB transfer and Time To Interactive <= 2 seconds on 3G When a submission POST fails due to connectivity loss Then the submission is queued locally with an idempotency key and shown as “Queued” When connectivity resumes Then queued submissions auto-sync within 30 seconds and show “Sent” without user re-entry And duplicate re-taps do not create duplicate records server-side
Batch Inventory Tracking and Usage Metrics
Given a PDF sheet is generated Then a Batch record is created with unique batch ID, total codes, window, bucket size, flow, and creator When codes from the batch are scanned Then the dashboard shows counts of scanned-by-minute, used vs remaining, and last activity timestamp in near real time (<= 60 seconds delay) And admins can export batch usage as CSV
PDF Watermarking and Security Metadata
Given a sheet is generated Then each page displays a diagonal watermark with organization name and batch ID at 10–15% opacity that does not obstruct QR readability And each code cell shows the minute label and batch ID And the PDF embeds document metadata (Title, Author=Organization, Subject=Batch ID) for audit
Compromise Response: Revoke Remaining Codes
Given an admin marks a batch as Compromised When revocation is confirmed Then all codes whose validity start time is now or in the future are revoked within 30 seconds And scans of revoked codes display a Revoked page and return HTTP 403 And codes already scanned and in-progress submissions remain submittable for up to 30 minutes after revocation And the action is logged with actor, timestamp, and reason, and can be undone (Unrevoke)
Time Zone and DST-Accurate Minute Labels
Given the admin selects a time zone and a window that may cross a DST boundary When the PDF is generated Then each minute label aligns with the selected time zone offset at that minute and includes the zone abbreviation And in fall-back overlaps the two identical local hours are disambiguated with offset (e.g., -0400/-0500) And in spring-forward gaps the missing minutes are omitted with no duplicate labels
Scan Telemetry & Impact Board Integration
"As a program director, I want live metrics on scans and conversions so that I can tune rotation and staffing to maximize impact."
Description

Real-time capture of scan events, rotation cadence, replay blocks, and conversion outcomes, feeding both the adaptive rotation engine and GiveCrew’s Impact Board. Provides per-event dashboards with scans per minute, average wait proxy, toggle usage, code TTL distributions, and rejection reasons. Supports alerts on anomalous replay spikes, anonymized aggregation for A/B of interval settings, and retention policies with opt-out controls to respect privacy.

Acceptance Criteria
Real-time Scan & Rotation Telemetry Capture
Given an active SmartRotate session When a QR code is scanned or a rotation event occurs Then the system records an event with fields: timestamp_ms, rotation_id, code_id, code_ttl_at_scan, app_channel (Signup|Give), device_hash (salted SHA-256), geo_hint (if permission granted), event_type (scan|rotate), replay_flag, and request_id And the event is enqueued within 200 ms of receipt (p95) And the event is available to the adaptive rotation engine within 500 ms (p95) And the event is queryable by analytics within 5 seconds (p95) And the end-to-end event loss rate is < 0.1% per session
Replay Detection & Block Logging
Given a QR payload with single-use nonce and TTL When the same code_id+nonce is presented after first acceptance or after TTL expiry Then the request is rejected server-side with error_code=REPLAY_BLOCK and HTTP 409 And rotation cadence is not advanced by the rejected attempt And a replay_log event is written with fields: timestamp_ms, code_id, rotation_id, device_hash, reason (expired|duplicate|nonce_mismatch) And the rejection reason appears in the per-event dashboard within 10 seconds (p95) And false positive rate for replay detection is < 0.05% over 10,000 scans in test
Conversion Outcome & Toggle Attribution
Given a scan starts a session in Signup or Give When the user completes, abandons, or toggles between flows Then the system correlates the scan to outcome with fields: flow_type_final, completed (true|false), abandoned_stage, donation_amount_cents (if any), signup_success (true|false), time_to_convert_ms And each toggle event is captured with timestamp_ms and direction (Signup->Give or Give->Signup) And 99% of completed outcomes are linked to their originating scan within 15 minutes or marked timeout And attribution error rate (unlinked but completed) is < 0.5% per event day
Per-Event Dashboard Metrics Refresh
Given a live per-event dashboard view When scans, rotations, rejections, and outcomes occur Then scans_per_min (rolling 60s) updates at least every 5 seconds and matches backend API value within 1% And avg_wait_proxy updates at least every 5 seconds and equals the backend-computed metric for the same window And toggle_usage_rate (#toggles/#scans) updates within 5 seconds and matches backend within 1% And code_TTL_distribution histogram for the last 15 minutes matches backend bin counts exactly And rejection_reasons breakdown reflects replay and other errors within 10 seconds (p95) And initial dashboard load completes in ≤2 seconds on 4G reference network
Anomalous Replay Spike Alerts
Given baseline replay_rate is computed as a rolling 15-minute average When replay_rate exceeds 2% for at least 2 consecutive minutes or z_score > 3 versus the prior 24h same local time window Then an alert is generated within 60 seconds with payload: event_id, window, replay_rate, scan_rate, recent_reasons, links to dashboard And notifications are delivered to in-app alerts immediately and to email/webhook if configured And deduplication ensures ≤1 alert per 10 minutes per event unless replay_rate increases by ≥50% And the alert auto-resolves when replay_rate remains below 1% for 10 consecutive minutes
Impact Board Real-Time Integration
Given active events producing scans and conversions When aggregated metrics change Then the Impact Board updates total scans, signups, donations_count, and donation_amount within 10 seconds (p95) And the Progress Poster view renders without any PII and is printable to A4/Letter And Impact Board aggregates for the last hour equal the sum of per-event metrics within 0.5% And if the Impact Board is temporarily offline, updates are queued and replayed within 60 seconds of reconnection
Privacy, Anonymization, Retention & Opt-Out Controls
Given telemetry processing for SmartRotate When events are stored and aggregated Then device identifiers are hashed with a rotating daily salt and raw IP addresses are discarded after coarse geolocation (city-level) And a user opt-out flag suppresses analytics storage and A/B inclusion while preserving basic functionality And exported A/B datasets are anonymized with k-anonymity k ≥ 20 and contain no direct identifiers And raw event retention is 30 days and aggregated metrics retention is 365 days And delete requests remove matching records from analytics within 24 hours and from backups within 7 days

DupShield

On-device duplicate and spam protection that works fully offline. Uses privacy-safe local hashing to catch repeated entries, throttle rapid-fire scans, and flag likely duplicates for review—then auto-merges cleanly on sync. Stewards spend less time de-duping; recruiters capture more real people and fewer junk records.

Requirements

On-device Hashing & Fingerprinting
"As a field organizer, I want the app to locally recognize likely duplicate signups without sending private data anywhere so that I can work offline and protect supporter privacy."
Description

Implement a privacy-preserving, fully offline fingerprinting service that normalizes key contact fields (name, phone, email, address) and generates multiple hash-based tokens and phonetic keys to enable exact and fuzzy matching without storing raw PII. Use per-org peppers stored in secure enclave (Keychain/Keystore) with rotation on sync and backward-compatible decoding to maintain matchability across epochs. Maintain a compact, query-optimized local index for sub-100ms lookups on low-end Android devices, with memory and battery safeguards. Ensure deterministic behavior across app restarts, resilience to partial data entry, and locale-aware normalization (diacritics, transliteration). Expose a simple SDK to capture, query, and update fingerprints from all intake surfaces (forms, QR/NFC scans, shift check-ins) entirely offline.

Acceptance Criteria
Locale-Aware Normalization & Deterministic Fingerprinting
- Given name, email, phone, and address inputs with variations in case, diacritics, punctuation, and whitespace, When normalized, Then outputs use Unicode NFC, locale-aware casefolding, collapsed whitespace, and diacritics removal for matching keys. - Given "José Núñez" and "Jose Nunez", When phonetic keys are generated, Then the keys are identical. - Given "Алексей" and "Aleksei" under configured transliteration, When normalized, Then name matching keys are identical. - Given an app restart, When the same raw inputs are hashed, Then all fingerprint tokens are byte-identical. - Given address variants "123 Main St., Apt 2B" and "123 main street apt 2b", When address keys are generated, Then they are identical.
No Raw PII at Rest via Pepper-Based Hashing
- Given fingerprint generation, When persisting artifacts, Then no raw PII is written to disk, logs, or analytics. - Given completion of hashing, When inspecting process memory, Then raw PII buffers are zeroed within 100ms. - Given stored artifacts, When attempting reversible decoding without original PII, Then reconstruction is impossible; only salted/peppered hashes and phonetic keys exist. - Given debug and error logs, When fingerprinting runs, Then no raw PII appears in logs.
Pepper Storage, Rotation, and Cross-Epoch Matchability
- Given a new org, When initializing, Then a unique per-org pepper is generated and stored in hardware-backed KeyStore/Keychain as non-exportable. - Given a sync-triggered rotation, When a new pepper is activated, Then the previous 2 peppers are retained encrypted for matching, and rotation is atomic. - Given records hashed before and after rotation, When queried, Then duplicates still match across epochs via backward-compatible decoding. - Given device backup/restore, When app data is restored on the same device, Then the pepper remains intact and unreadable outside secure storage. - Given an attempt to access the pepper from outside the app process, When executed, Then access is denied.
Sub-100ms Local Lookup on Low-End Android
- Given an Android Go class device (2GB RAM) with 50k indexed contacts, When calling query() with 1–3 keys, Then p95 latency is <100ms and p99 <150ms. - Given intake of a new record, When generating fingerprints, Then p95 generation time per record is <30ms. - Given the on-device index for 50k contacts, When measured on disk, Then size is <=50MB and build/rebuild completes in <=60s. - Given normal operation, When measuring memory, Then peak incremental RSS during lookup stays <=64MB and no main thread is blocked >16ms (no ANR). - Given background maintenance, When scheduled, Then it runs only under OS low-power allowances and yields to user interaction within 50ms.
SDK Coverage Across All Offline Intake Surfaces
- Given the SDK, When integrating, Then capture(), query(), update(), and upsertIndex() APIs are available with documented inputs/outputs and error codes. - Given a device in airplane mode, When invoking any SDK API, Then the call succeeds without network access and produces deterministic results. - Given intake events from forms, QR scans, NFC scans, and shift check-ins, When processed through the SDK, Then fingerprints are generated and duplicate candidates returned consistently. - Given concurrent intake events (>=4 threads), When using the SDK, Then operations are thread-safe and free of data races. - Given an invalid payload, When calling the SDK, Then a specific error code is returned and no partial writes occur.
Exact and Fuzzy Match Token Generation Quality
- Given a labeled test corpus of >=10k contacts with exact and near-duplicate pairs, When evaluating, Then exact-match tokens achieve recall >=99% on exact duplicates and precision >=99%. - Given the same corpus, When evaluating fuzzy tokens (phonetic/name distance, email variants, phone formatting), Then recall >=95% and precision >=98% on near-duplicates. - Given email addresses with plus-addressing and dot-variants on providers that ignore them, When generating email match keys, Then variants map to the same key. - Given phone numbers with locale-specific formatting, When normalized offline, Then E.164-or-equivalent canonical keys are produced where country can be inferred; otherwise, formatting noise is removed and matching remains robust. - Given adversarial collisions tests, When hashing, Then no keyspace hot spots exceed 0.5% of total entries for any single token type at 50k scale.
Partial Data Handling and Offline Fault Tolerance
- Given only one field is present (e.g., phone only), When generating fingerprints, Then partial tokens are produced and query() utilizes available keys. - Given all fields are empty or invalid, When calling capture(), Then INVALID_INPUT is returned and no tokens are persisted. - Given malformed inputs (e.g., overlong strings, invalid UTF-8), When processed, Then the service sanitizes or truncates to spec without crash. - Given an app restart or crash during indexing, When the app relaunches, Then the index recovers without corruption and fingerprint outputs remain deterministic. - Given any API call, When measured, Then p95 completion time <=100ms under offline conditions with partial data.
Offline Real-time Duplicate Alerts
"As a volunteer recruiter, I want immediate guidance when I’m about to enter a duplicate so that I avoid cluttering the list and can keep moving quickly at the table."
Description

Provide non-blocking, real-time alerts during data capture that surface likely matches from the local index as the user types or scans. Show a concise banner with confidence score and top candidate(s), offering one-tap attach-to-existing or continue-as-new. Handle partial entries and low-signal inputs gracefully with progressive enhancement (e.g., escalate when both phone and name are present). Ensure accessibility (screen reader labels, color contrast), haptic feedback, and translation readiness. Never require network; degrade to quiet mode if local index is unavailable. Log user choices for later audit and model tuning.

Acceptance Criteria
Non-blocking Real-time Alerts During Entry
Given the local dedupe index is available When the user types in name, email, or phone fields or scans a code Then if any candidate has confidence >= 80, show a non-blocking alert banner within 250 ms and keep the input caret active And if no candidate has confidence >= 80, do not show an alert banner And during scanning, show the alert banner within 300 ms after the scan payload is parsed And the user can continue typing or scanning without additional latency or input being blocked
Banner Content and One-tap Actions
Given a high-signal duplicate alert is triggered Then the banner displays the top candidate’s name, a masked primary identifier (e.g., phone), and a numeric confidence score (0–100 rounded) And shows up to 3 candidates with an affordance to view all if more exist And provides two actions labeled "Attach to existing" and "Continue as new" When the user taps "Attach to existing" Then link the in-progress capture to the selected existing record and dismiss the banner within 300 ms without creating a new duplicate record When the user taps "Continue as new" Then proceed without linking to an existing record and dismiss the banner within 300 ms
Progressive Enhancement for Low-signal Inputs
Given only a single low-signal field is present (e.g., partial name or phone < 7 digits) When a potential match is found Then show a soft hint banner (no haptic) only if confidence >= 95 Given two or more high-signal fields are present (e.g., name + full phone or email) When a potential match is found Then show a prominent alert banner (with haptic) if composite confidence >= 80 And as additional fields are entered, update or escalate the banner within 250 ms without losing user input focus
Accessibility and Haptic Feedback Compliance
Given a screen reader is enabled (VoiceOver/TalkBack) When the banner appears Then it has role "alert" and is announced once as "Possible duplicate, {score}% match with {name}" And action buttons expose accessible names "Attach to existing" and "Continue as new" with role "button" And focus is not stolen; the current input remains focused and the banner is reachable next in reading order And interactive targets are at least 44x44 dp and text contrast ratio is >= 4.5:1 (icons/buttons >= 3:1) Given haptics are enabled When a high-signal alert is shown Then trigger a single light impact haptic When the user confirms "Attach to existing" Then trigger a single medium impact haptic
Translation Readiness and RTL Layout
Given the device language is changed to Spanish (es) and Arabic (ar) When an alert banner is shown Then all static strings are localized via i18n resources with no hard-coded English And the layout mirrors correctly in RTL (ar), including button order and icon alignment And on narrow screens (<= 360 dp), text truncates with ellipsis without overlap or clipping And no string exceeds the banner container; long names wrap to max 2 lines
Offline Operation and Quiet Mode Fallback
Given the device is offline When the user types or scans Then no network requests are made for duplicate checks and all matching uses the on-device index Given the local index is unavailable or not yet built When the user types or scans Then the system remains in quiet mode (no alerts shown) and data entry is uninterrupted And a diagnostic event "dup_index_unavailable" is logged once per session When the local index becomes available mid-session Then real-time alerts resume without requiring an app restart
Audit Logging of Alert Outcomes
Given any duplicate alert is shown When it is shown, updated, acted on, dismissed, or ignored for 5+ seconds Then write a local audit log entry with: timestamp (UTC), device_id, session_id, candidate_ids (hashed), top_score, action in ["shown","updated","attach","continue","dismiss","timeout"], input_fields_present, index_version And audit entries are stored locally and persist across app restarts And entries are queued for sync when connectivity returns And logging does not block the UI and completes within 50 ms per event
Spam/Rapid-Entry Throttling
"As a site lead, I want the app to slow or challenge suspicious bursts of entries so that we reduce junk records without blocking legitimate high-traffic moments."
Description

Detect and throttle suspicious rapid-fire submissions (e.g., repeated scans or bot-like form bursts) entirely offline using adaptive rate limits, per-device backoff, and entropy checks on fields (e.g., repeated domains, sequential numbers). Provide human-presence challenges suited for offline use (press-and-hold, simple pattern gesture) when thresholds are exceeded. Offer unobtrusive UI with clear recovery paths and admin-configurable policies to balance capture speed with data quality. Record throttling events locally for sync-time telemetry without including raw PII.

Acceptance Criteria
Per-Device Burst Throttling on Rapid Submissions
Given the device is offline and policy RateLimit.windowSeconds=10 and RateLimit.maxSubmissions=6 for recordType=signup When the user submits 6 or more signups within any 10-second window on the same device Then throttling is engaged within 100 ms and additional submissions are blocked for Throttle.durationSeconds=60 And a non-modal banner appears with a visible countdown and local error code=THROTTLED for attempted submissions And no app crash occurs and partially entered data in the form is preserved And when the countdown reaches zero, submissions succeed without additional prompts
Exponential Backoff After Repeated Threshold Breaches
Given Backoff.initialSeconds=15, Backoff.multiplier=2, Backoff.maxSeconds=300, Backoff.resetIdleSeconds=120 When the user triggers throttling 3 times within a 10-minute period Then the enforced wait times become 15s, then 30s, then 60s respectively, not exceeding 300s And if no submissions occur for 120s, the backoff resets to the initial 15s on the next breach And the UI countdown reflects the current backoff accurately to the second
Low-Entropy and Pattern-Based Spam Detection
Given the device maintains a rolling window of the last 20 offline submissions in memory only And Entropy.thresholdScore=0.7 When any of the following are detected: (a) >80% of emails share the same domain in the last 20, (b) 3+ sequential phone numbers across the last 5 entries, (c) Shannon entropy of concatenated name+email+phone <2.5 bits/char Then an entropy score is computed and if score >=0.7 the submission is flagged as likely spam And the user is prompted with a human-presence challenge before the record can be saved And no raw field values are persisted to any spam log
Offline Human-Presence Challenge to Bypass Throttle
Given Challenge.types=[press_and_hold_2s, pattern_3_points] and Challenge.successBypassMinutes=5 and Challenge.maxFailuresBeforeLock=3 and Challenge.lockoutSeconds=60 When throttling or low-entropy detection is active Then the user may choose a challenge CTA that opens an offline challenge sheet And a successful challenge within 10s allows the next 20 submissions or 5 minutes (whichever comes first) without further throttling And each failed attempt increments a counter and after 3 failures a 60s lockout is enforced with a clear message And all challenge flows are operable without network and with screen reader labels
Unobtrusive UI With Clear Recovery Paths
Given throttling is active while the user is filling the form When the user continues interacting with fields Then the banner does not block input, the primary action shows a disabled state with a countdown, and an accessible hint explains the reason And the user can either wait for the countdown, solve a challenge, or save the draft locally without data loss And upon recovery (countdown completed or challenge passed), the primary action re-enables automatically and submits successfully on first tap
Admin-Configurable Policies With Offline Enforcement
Given a steward with Admin role opens DupShield settings while offline When they update RateLimit.windowSeconds (1–60), RateLimit.maxSubmissions (1–50), Backoff.initialSeconds (5–60), Backoff.maxSeconds (60–900), Entropy.thresholdScore (0.1–0.95), and Challenge.types Then inputs outside allowed ranges are prevented with inline validation And saved policies persist locally within 200 ms and begin governing throttling immediately without network And non-admin users cannot view or change these settings
Local Throttling Telemetry Without Raw PII
Given a throttling or challenge event occurs offline When the event is logged locally Then the log record contains only: eventType, timestamp (minute-level), recordType, deviceIdHash, policyVersion, counts (attempts, throttled, challengesPassed/Failed), and no raw field values And any field-derived signals (e.g., domain) are stored only as salted one-way hashes scoped to the device session And the total unsynced telemetry storage remains <=256 KB and older records are pruned FIFO And upon next sync, telemetry uploads successfully without including raw PII
Duplicate Review & Merge Console (On-Device)
"As a data steward, I want a simple queue to review and resolve possible duplicates on my phone so that our database stays clean even before I reconnect."
Description

Introduce an on-device review queue that aggregates flagged duplicates created while offline. Provide a side-by-side comparison view with field-level merge controls, primary record selection, confidence indicators, and previews of downstream effects (e.g., shifts, donations). Support undo, audit notes, and role-based access (stewards vs. recruiters). Merges must preserve identifiers, link related objects, and maintain history for compliance. Operate fully offline and reconcile decisions during next sync.

Acceptance Criteria
Offline Duplicate Queue Aggregation with Confidence
Given the device is offline and a new person record is saved When its local hash matches an existing record above the duplicate threshold Then the pair is added to the on-device "Review Duplicates" queue within 2 seconds. Given multiple flagged pairs exist When the queue is opened Then items display name, top matching fields, confidence score (0–100), and timestamp without triggering any network requests. Given confidence scores exist When the user sorts by Confidence or Recency Then the list reorders accordingly and filtering by score range is applied locally. Given the app is force-closed When relaunched offline Then the queue contents and sort/filter state persist exactly. Given the queue exceeds 100 items When scrolling Then the initial render occurs within 500 ms and subsequent page loads within 300 ms.
Side-by-Side Comparison and Field-Level Merge Controls
Given a queue item is opened When viewing the pair Then both records render side-by-side with aligned fields and per-field selection controls (Left, Right, or Custom). Given a text field requires editing When Custom is selected Then an editable input appears with validation enforced before merge. Given system-managed fields (e.g., record_id, created_at) When viewing controls Then those fields are read-only and cannot be overridden. Given at least one field has conflicting values When the user taps "Auto-select by confidence" Then the higher-confidence source is preselected per field and all selections are visibly highlighted. Given invalid or incomplete selections exist When the user taps Merge Then the action is blocked with inline error messages identifying each offending field.
Primary Record Selection and Identifier Preservation
Given two or more candidate duplicates When the user selects a primary record Then that record retains its record_id and external identifiers. Given secondary records exist When the merge completes Then each secondary is marked with merged_into=primary_id and is hidden from search by default. Given related objects (shifts, donations, notes) reference secondaries When the merge completes Then all references are re-pointed to the primary in the local store. Given the merge modifies identifiers When audit data are recorded Then the mapping of old->new references is stored to support compliance history and future rollbacks.
Downstream Effects Preview Before Merge
Given a duplicate pair is open When the user taps "Preview Effects" Then counts of related objects to be re-linked (upcoming shifts, past shifts, donations, pledges, notes) are shown along with any potential conflicts. Given conflicts would occur (e.g., duplicate upcoming shift slot) When the preview is displayed Then the console shows the conflict type and a selectable resolution (skip re-link, merge related, or keep both where allowed). Given the user confirms the merge When the action executes Then only the previewed changes are applied and a summary matches the preview exactly.
Undo Merge and Audit Notes (Offline)
Given a merge is completed offline When the confirmation banner appears Then the user can Undo the merge until the next successful sync or for up to 24 hours, whichever comes first. Given the user taps Undo before expiration When processed Then all field values, related object links, and record visibility are restored to their exact pre-merge state and a reversal audit entry is recorded. Given a merge touches any donation records When attempting to complete the merge Then an Audit Note entry (free text, minimum 10 characters) is required before the Merge button is enabled. Given a merge (or undo) occurs When audit data are saved Then the log captures user id, role, device id, timestamp, pair ids, selected fields, and before/after summaries, all stored locally for sync.
Role-Based Access: Stewards vs Recruiters
Given a user with Recruiter role When opening the Review Duplicates queue Then they can view and comment but cannot finalize merges; the Merge button is disabled with a tooltip explaining required role. Given a user with Steward role When opening any queue item Then all merge controls are enabled. Given a device is offline and the cached role assignment is older than its offline TTL When attempting to merge Then the action is blocked until re-authentication on next connectivity. Given a permission change is received on sync When the next session starts Then capabilities update accordingly without requiring a reinstall.
Offline Sync Reconciliation and Conflict Handling
Given one or more merges were performed offline When the device next syncs Then all merge decisions are uploaded in order and applied server-side without user intervention. Given the server indicates the pair was already merged differently When reconciling Then the client performs a three-way merge and either confirms no-op or creates a conflict task in the queue marked "Sync Conflict." Given reconciliation succeeds When local data are updated Then record histories, identifier mappings, and audit logs are marked as synced with server-issued version ids. Given any reconciliation fails When sync completes Then the user is shown a non-blocking alert and affected items are queued for retry with exponential backoff.
Deterministic Auto-Merge on Sync
"As an operations manager, I want duplicates to auto-merge safely when devices sync so that staff spend less time cleaning data and more time organizing."
Description

When connectivity returns, reconcile local records against the server and other devices using hash exchanges and deterministic merge rules prioritized by data completeness, recency, and verified fields. Ensure idempotent, conflict-safe operations that can be retried without duplication. Preserve audit trails, maintain referential integrity across donations, shifts, and communications, and emit event logs/webhooks for downstream systems. Provide dry-run simulation and rollback for support, and produce a post-sync summary (merged, skipped, conflicts) for steward review.

Acceptance Criteria
Privacy-Safe Hash Exchange and Deterministic Merge Rules
- Given a device reconnects after offline use with overlapping local and server records, When sync starts, Then only salted, truncated hashes of match keys (email, phone, name+DOB, address) and record fingerprints are exchanged; no raw PII leaves the device. - Given potential matches are identified by hash equality or fuzzy-hash similarity ≥ 0.90, When field-level conflicts are detected, Then the winner for each field is chosen by this deterministic priority: verified value > higher completeness score > newer updatedAt (ms) > lower lexicographic recordId. - Given identical inputs across devices, When two devices compute merges independently, Then they produce the same survivor recordId and the same per-field winners. - Given a tie-break is applied, Then the applied rule and tiebreak key are recorded in metadata for that field. - Given a record does not meet match thresholds by hashes, When sync runs, Then it is created as a new entity without modifying existing records. - Given 5,000 candidate records, When matching and merge planning runs on a mid-tier device, Then it completes within 5 seconds.
Idempotent Retry Without Duplication
- Given a network failure during sync, When the same batchId/idempotencyKey is retried, Then no duplicate creates/updates occur and the final state equals a single successful run. - Given the same batch is replayed up to three times, When webhooks are emitted, Then eventIds/idempotencyKeys are reused and no duplicate downstream events are observed. - Given two devices sync conflicting updates concurrently, When conflict resolution executes, Then exactly one survivor commit is applied per record version; losing updates are deterministically folded in and no duplicate child records are created. - Given retries occur, When sequence numbers are assigned per tenant, Then they remain strictly increasing with no gaps caused by retries.
Audit Trail and Change Log Preservation
- Given any auto-merge occurs, When audit is written, Then the entry includes: survivor recordId, mergedIds[], per-field before/after (PII masked/hashed), source deviceIds[], ruleApplied per field, userId if manual override, correlationId, occurredAt. - Given a steward queries by recordId or correlationId, When requesting the last 10,000 entries, Then results return within 500 ms. - Given a 365-day retention policy, When the window elapses, Then entries are archived (not deleted) and remain retrievable on request; no audit entries are altered within the retention window. - Given sensitive values (email, phone), When stored in audit, Then only hashed values and last-4 are retained; raw values are not stored.
Referential Integrity Across Related Records
- Given two Person records are merged, When the survivorId is chosen, Then all related donations, shifts, and communications referencing losingIds are repointed to survivorId in the same transaction. - Given any referential update fails, When the transaction ends, Then the entire merge is rolled back and reported as a conflict; zero orphaned child records remain. - Given a post-merge integrity sweep runs, When it completes, Then it reports zero dangling references across donations, shifts, and communications; otherwise the batch is rolled back. - Given local and server stores enforce foreign keys, When merges apply, Then all constraints remain valid post-sync.
Event Log and Webhook Emission on Merge
- Given a successful auto-merge, When events are generated, Then an entity.merged event is logged and a webhook is sent with payload: tenantId, entityType, survivorId, mergedIds[], fieldChanges[], rationale[], correlationId, sequence, occurredAt, eventId/idempotencyKey, signature. - Given webhook delivery fails, When retries occur, Then exponential backoff is used for up to 24 hours with a maximum of 10 attempts, and delivery order is preserved by sequence. - Given downstream deduplicates by eventId or idempotencyKey, When retries happen, Then no duplicate effects are observed downstream. - Given a batch with 100 merges, When inspecting the event log, Then exactly 100 corresponding events exist and can be replayed by sequence range.
Dry-Run Simulation and Batch Rollback
- Given dryRun=true is requested, When the merge plan is computed, Then the response includes counts (toMerge, toCreate, toSkip, conflicts) and per-record diffs/rationales, with no writes or webhooks emitted. - Given a merge batch is committed, When a rollback token (TTL 24h) is used, Then all changes in the batch are reverted, child references are restored, and entity.merge.reverted events are emitted. - Given a rollback completes, When inspecting audit, Then a reversal entry links to the original correlationId; no partial rollbacks occur. - Given a dry-run is executed followed by commit without intervening data changes, When results are compared, Then counts and winners match; if data changed, a warning is returned and a re-run is required.
Post-Sync Summary for Steward Review
- Given sync completes, When the steward opens the summary, Then totals for merged, created, skipped, and conflicts are shown with filterable, sortable lists by entity type, rule applied, and date. - Given 5,000 processed records, When rendering the summary on a mid-tier device, Then it loads within 3 seconds and remains available offline after generation. - Given conflicts exist, When reviewing, Then deterministic suggested resolutions are presented and can be applied individually or in bulk. - Given an export is requested, When CSV and JSON are generated, Then counts and record identifiers match across both formats.
Admin Controls & Policy Tuning
"As an admin, I want to configure how aggressively duplicates are detected and merged so that the system fits our context without blocking real supporters."
Description

Expose organization-level settings to tune similarity thresholds, throttling sensitivity, challenge types, and merge precedence rules. Allow per-campaign overrides and safe presets (Conservative/Balanced/Aggressive) with inline guidance. Provide allow/deny lists (e.g., disposable email domains), locale-specific normalization options, and privacy controls (e.g., hashing epochs, retention for fingerprints). Ensure changes versioned and synced to devices with staged rollout and metrics to assess impact on duplicate rate and capture friction.

Acceptance Criteria
Preset Selection and Threshold Tuning at Org Level
Given an org admin opens DupShield Policy settings When the admin selects preset "Balanced" Then default similarity thresholds populate as Name=0.82, Email=0.90, Phone=0.95 And inline guidance tooltips are visible for each threshold control When the admin adjusts any threshold within 0.50–0.99 and clicks Save Then values are validated, saved, and immediately reflected as the org default And the saved values persist after page reload and new session sign-in
Per-Campaign Overrides and Inheritance Controls
Given Campaign A is set to Inherit org defaults When the admin toggles Override for Campaign A Then override fields become editable and prefilled with current org defaults When the admin sets Name threshold to 0.88 for Campaign A and saves Then Campaign A uses 0.88 while other campaigns continue using the org default When the admin clicks Reset to Inherit for Campaign A Then Campaign A reverts to org defaults within 2 seconds and override indicator clears And a versioned audit entry records timestamp, actor, scope=Campaign A, and changed fields
Throttling Sensitivity Configuration and Offline Enforcement
Given the org throttling preset is set to "Conservative" with limit=5 entries per 30 seconds per device When a device records 6th entry within 30 seconds while offline Then the 6th attempt is blocked with a "Slow down" banner and code DS-THROTTLE When the preset is changed to "Aggressive" (limit=2/30s) and devices reconnect Then updated throttling takes effect on-device within 15 minutes and enforces offline When a custom limit 1–20 and window 10–120s are entered Then out-of-range values are rejected with inline validation and Save is disabled
Challenge Type Selection for Suspected Spam
Given the admin selects challenge type "Tap-and-hold 2s" for risk >= Medium When a suspected spam entry is captured offline Then a tap-and-hold control appears and must be held uninterrupted for 2 seconds to proceed If the challenge fails 3 times within 60 seconds Then capture is blocked, logged as event challenge_fail, and the session is throttled for 60 seconds Given available challenge types are Tap-and-hold, Emoji match, and 1-digit sum When one is selected and saved Then only the selected type is enforced and the UI meets WCAG 2.1 AA contrast
Merge Precedence Rules and Conflict Resolution on Sync
Given merge precedence is configured as Phone=device>server, Email=server>device, Name=most_recent_updated When a device syncs and a duplicate is detected Then the merged record applies the precedence rule per field And a merge log entry shows old_value, new_value, source, and timestamp per changed field If two updates share identical timestamps Then a deterministic tiebreaker using lexicographic device_id is applied and recorded And the admin can export a CSV of merges for the past 7 days
Allow/Deny Lists and Locale-Specific Normalization
Given "mailinator.com" is added to the email deny list and "+999" to phone country deny list When an offline entry matches either rule Then submission is blocked with inline reason and code DS-DENY-EMAIL or DS-DENY-PHONE Given "volunteer.myorg.org" is on the allow list When an email matches both allow and deny lists Then the allow list takes precedence and submission proceeds Given locale es-MX normalization is enabled When entering "José" and "Jose" Then duplicate checks treat them as equal and the behavior persists offline
Policy Versioning, Staged Rollout, and Impact Metrics
Given the admin publishes Policy v1.3 with a 20% staged rollout to Org X When eligible devices reconnect Then 20% ±2% of devices receive v1.3 within 15 minutes; others remain on the prior version When the admin initiates rollback Then affected devices revert to the prior policy within 15 minutes and rollout status shows Rolled Back Then the dashboard displays duplicate rate, throttle blocks, challenge fail rate, and capture rate by policy version, campaign, and cohort with data latency ≤15 minutes And an A/B view shows delta and 95% CI for duplicate rate once each cohort has ≥200 submissions

Kiosk Lock

Single-app lockdown with PIN/gesture unlock to keep pop-up stations tamper-proof. Auto-hides sensitive fields after each submission, blocks app switching, and survives accidental closes or low-battery restarts with autosave. Perfect for tables, doors, and rallies where strangers may handle the phone.

Requirements

Cross-Platform Single-App Lockdown
"As a field lead, I want the device locked to GiveCrew so that attendees can’t tamper with settings or open other apps."
Description

Enforces a true single-app mode that prevents leaving GiveCrew during kiosk sessions. Implements OS-native controls (Android Lock Task Mode/screen pinning; iOS Guided Access) with in-app guards to block app switching, system gestures, notifications overlays, external intents/URLs, and screenshot/recording where possible. Maintains a strict in-app navigation whitelist so camera/QR and payment subflows remain contained. Presents step-by-step enablement prompts and checks for prerequisites at runtime. Integrates with session state so lockdown begins when kiosk mode is enabled and remains active until authorized exit, with minimal performance/battery impact.

Acceptance Criteria
Native Single-App Enablement (iOS Guided Access & Android Lock Task/Pinning)
Given a supported device, when the organizer enables Kiosk Lock in settings, then the app detects platform and strongest available single‑app control (iOS Guided Access, Android Lock Task; else Android Screen Pinning) and starts it. Given iOS, when Kiosk Lock is enabled, then if Guided Access is off or no passcode exists the app presents a step-by-step activation guide and blocks session start until UIAccessibility.isGuidedAccessEnabled == true. Given Android with Lock Task permission, when Kiosk Lock is enabled, then startLockTask() is invoked and pressing Home/Recents has no effect; status bar expansion is disabled. Given Android without Lock Task permission, when Kiosk Lock is enabled, then the app guides the user to enable Screen Pinning and a secure lock screen; upon return, screen pinning starts and attempting to unpin requires the device PIN. Given success, then the app shows an in-app banner “Kiosk Lock active” and logs the activation event with device, OS, and control mode.
Prevent App Switching and System UI Escape Paths
Given an active kiosk session, when the user presses Home/Recents/Back or performs system swipe gestures, then the app remains in the foreground and no task switcher is shown (Android Lock Task; iOS Guided Access enforces this). Given iOS, when the Side button is triple-pressed, then the Guided Access passcode screen appears and the app is not exited without the correct passcode. Given Android, when the user attempts split‑screen, picture‑in‑picture, Quick Switch (Alt+Tab gesture), or Assistant long‑press, then these actions are blocked in Lock Task and the app remains focused. Given kiosk session, when the user tries to expand the status/notification shade or Control Center, then expansion is blocked (Android Lock Task) or disabled by Guided Access (iOS). Given kiosk session, when the user attempts to take screenshots/recordings, then on Android the capture is blocked via FLAG_SECURE (black output); on iOS, a privacy shield obscures sensitive views during capture/app switcher snapshots.
Block Notifications Shade and Overlays During Kiosk Session
Given an active kiosk session, when a notification arrives, then the notification shade cannot be expanded and any heads‑up/banner cannot take the user out of the app if tapped. Given an active kiosk session, when a system dialog or permission prompt appears, then it is modal and returns control to the app upon dismissal without exposing the home screen or other apps. Given Do Not Disturb/Focus is off, when Kiosk Lock starts, then the app prompts the organizer with device‑specific guidance to reduce interruptions and confirms completion before continuing the session.
Contain Navigation: External Intents/URLs and Whitelisted Camera/Payment Subflows
Given an active kiosk session, when a link to an external host is tapped, then it opens only in an in‑app constrained webview/custom tab limited to approved domains; non‑approved domains are blocked with a message and no app switch occurs. Given an active kiosk session, when an intent/URL targets an external app (e.g., mailto:, tel:, geo:), then the app intercepts and either handles in‑app (where safe) or blocks with “Not available in Kiosk”. Given QR scan flows, when camera access is needed, then an in‑app camera view is used and no external camera chooser is shown; denying the permission returns to the prior screen without leaving the app. Given payment flows, when the user proceeds to checkout, then only whitelisted payment provider views/hosts are allowed and closing the view reliably returns to the prior screen inside GiveCrew. Given any navigation attempt outside the whitelist, then the app logs the blocked action and maintains kiosk state.
Guided Setup With Prerequisite Checks and Fallbacks
Given the organizer taps Enable Kiosk Lock, when prerequisites are evaluated, then the app shows a checklist with real‑time pass/fail for: device passcode set, iOS Guided Access availability, Android Lock Task capability, screen pinning enabled (fallback), camera/payment whitelist configured. Given any prerequisite fails, then the Start button is disabled and contextual step‑by‑step instructions are presented (with deep links where supported) until all required checks pass or an approved fallback is selected. Given all required checks pass, when Start is tapped, then kiosk session begins within 2 seconds and the checklist shows all green with a timestamped confirmation.
Session Persistence and Authorized Exit Control
Given an active kiosk session, when the app is force‑closed or the device reboots and the app is relaunched, then the app detects an active kiosk session state and re‑enters single‑app lock automatically within 5 seconds of launch before any user can navigate away. Given an active kiosk session, when an exit is requested, then the app requires an admin PIN/gesture; with 5 consecutive failures, a 60‑second cooldown is enforced and an audit log entry is created. Given iOS, when ending Guided Access, then the iOS Guided Access passcode is required; the in‑app admin PIN must also be validated before the session state is cleared. Given Android, when stopping Lock Task/Screen Pinning, then the in‑app admin PIN must be validated before calling stopLockTask()/unpinning and clearing session state. Given successful exit, then all kiosk restrictions are released, an audit event is logged, and the UI returns to the normal home screen.
Performance and Battery Overhead Limits During Kiosk
Given a 30‑minute idle kiosk session on reference devices (mid‑range Android, recent iPhone), then additional CPU usage averages ≤5% and memory overhead ≤150 MB versus baseline app idle. Given a 60‑minute active kiosk session with intermittent camera and webview usage, then battery drain attributable to the app is ≤3% per hour on reference devices. Given starting or ending kiosk mode, then transition latency is ≤1 second to lock and ≤1 second to unlock. Given continuous QR scanning for 2 minutes, then frame render time p95 ≤32 ms and no more than 5% dropped frames.
Secure Unlock & Admin Exit Controls
"As an organizer, I want to quickly unlock the kiosk with a PIN to make changes and relock immediately so that setup changes are safe during events."
Description

Provides a configurable, secure admin exit from kiosk mode using a PIN or gesture unlock, with optional biometric on supported devices. Access is gated behind a discreet activation (e.g., long-press or multi-tap hotspot) to avoid discovery. Includes exponential backoff and temporary lockout on failed attempts, an emergency override path, auto-relock timers, and role-based access to a minimal admin panel (end session, check health, change forms). Ensures no previously entered attendee data is revealed during or after unlock.

Acceptance Criteria
Hidden Unlock Entry via Hotspot Gesture
Given kiosk mode is active with the default hidden hotspot enabled (bottom-right 44x44 px) and unlock gesture set to 5 taps within 3 seconds When a user taps outside the hotspot or performs fewer/more taps or exceeds the 3-second window Then no unlock UI appears And when the exact gesture is performed within the hotspot bounds and time window Then the unlock prompt appears within 300 ms And the unlock prompt obscures underlying content so no attendee data is visible
Multi-Factor Admin Unlock (Biometric + PIN/Gesture)
Given the device supports biometrics and the admin has enabled biometric unlock and set a 6-digit PIN When the unlock prompt appears Then biometric is offered first And on biometric success the minimal admin panel opens within 500 ms And on biometric failure/skip a masked PIN keypad appears And only the exact PIN or configured gesture unlocks And if no successful auth occurs within 5 minutes the attempt is canceled and the app re-locks to kiosk mode And at no point is previously entered attendee data displayed
Exponential Backoff and Temporary Lockout on Failed Unlocks
Given default backoff thresholds are active When consecutive failed unlock attempts occur Then enforce waits of 5s after the 1st failure, 15s after the 2nd, 45s after the 3rd, and 120s (cap) for subsequent failures And after 6 failed attempts within 15 minutes a 10-minute lockout is applied And the unlock UI displays a visible countdown for remaining wait/lockout time And the public-facing kiosk flow remains usable during backoff/lockout And each attempt is audit-logged with timestamp, method (biometric/PIN/gesture), outcome, and device ID
Emergency Override Auth Path During Lockout
Given the kiosk is in a lockout state and an authorized administrator is present When the hidden override gesture (3-second long-press on hotspot) is performed and the correct override code is entered Then the minimal admin panel opens within 1 second and is flagged as "Emergency" And all related events are audit-logged with reason "override" And backoff/lockout counters are reset And no attendee data is revealed before, during, or after the override
Auto-Relock Timers and Session Boundaries
Given the minimal admin panel is open When there is no interaction for the configured inactivity timeout (default 60s, configurable 15–300s) Then the app auto-relocks to kiosk mode And after completing any admin action (End Session, Check Health, Change Active Form) the app re-locks within 1 second And if the app is backgrounded, the screen turns off, or the device restarts, the app resumes locked in kiosk mode and requires unlock And app switching is only enabled after explicit "End Session" confirmation by an Admin
Role-Based Minimal Admin Panel with Data Non-Disclosure
Given a successful admin unlock has occurred When the minimal admin panel loads Then only users with role "Admin" can access it And only "End Session", "Check Health", and "Change Active Form" actions are available And attendee/donor records, submission history, and previously entered form data are not visible or reachable And exporting data and broader settings are disabled until the session is ended And all admin actions are audit-logged And on returning to kiosk mode, the public form is reset and sensitive fields are cleared/hidden
Post-Submission Privacy Reset
"As a volunteer captain, I want the kiosk to wipe the previous person’s info after submit so that the next person can’t see or reuse sensitive data."
Description

Automatically clears or masks all sensitive fields and any visible history immediately after each successful submission. Resets the app to a neutral start screen, disables autofill suggestions, clears clipboard, and prevents back-navigation to prior entries. Displays a generic confirmation for the attendee that times out and returns to the attract screen. Ensures compliance with privacy expectations at public stations and prevents accidental exposure of PII between consecutive users.

Acceptance Criteria
Sensitive Fields Cleared After Successful Submission
Given a completed form containing PII And the submission is valid When the user taps Submit And the server returns a 2xx success Then all text inputs are cleared to empty And dropdowns/selects reset to default And toggles/checkboxes/radios are reset to off/unchecked And signature canvases, photo previews, and attachments are removed from the UI And any on-screen recent activity/history panel is hidden And no PII is visible on screen or retrievable via scrolling And the reset completes within 1 second of the success response
Neutral Start Screen Reset With Timed Confirmation
Given a successful submission When the confirmation screen is shown Then it remains visible for 5 seconds (between 4 and 6 seconds) And on timeout the app navigates to the attract/start screen And the navigation/back stack is cleared so that back actions cannot return to the submitted form And the start screen is interactive within 500 ms of the timeout
Autofill and Suggestions Disabled in Kiosk Mode
Given kiosk lock mode is active And a user focuses a PII field (e.g., name, email, phone, address, notes) When the keyboard/input method opens Then no OS/browser autofill prompts, saved contact suggestions, or credential popups appear And previously entered values are not suggested in the field And repeating focus/blur 3 times produces no autofill or suggestions
Clipboard Sanitized on Submission
Given any field content was copied to the clipboard during entry And the submission is successful When the success response is received Then the system clipboard is cleared within 1 second And pasting within the app immediately after returns an empty value And no prior PII can be pasted into another field within the app after reset
Back-Navigation Blocked to Prior Entries
Given the app has returned to the attract/start screen after a submission When the user attempts to navigate back using hardware back, system gesture, or in-app back Then the app does not reveal the previously submitted form or its data And the user remains on the start screen or exits kiosk flow as configured And performing back action 3 times in succession still does not expose prior entries
Crash/Restart Privacy Immediately After Success
Given a submission succeeds And the device/app crashes, force-closes, or restarts within 2 seconds after success When the app relaunches in kiosk mode Then the app opens to the attract/start screen And no prior form values or submission details are visible And back navigation cannot reach the submitted form And any transient caches used for the submission are purged from the UI layer
Non-PII Generic Confirmation Content
Given a successful submission When the generic confirmation is displayed Then the confirmation contains no PII (no name, phone, email, address, donation amount, or identifiers) And the text is limited to generic success messaging (e.g., "Thank you") And the confirmation is accessible to screen readers without exposing PII
Autosave & Crash/Restart Recovery
"As a canvasser, I want the kiosk to recover the current signup after a low-battery restart so that we don’t lose data or time."
Description

Continuously and securely autosaves in-progress entries to encrypted local storage so partial data is preserved without exposing sensitive values beyond policy. On app close, crash, or low-battery/OS restart, GiveCrew auto-relaunches into kiosk mode and restores the last safe step, prompting the user to continue or discard. Queues completed submissions offline until connectivity returns and retries safely. Handles app updates gracefully with schema migration and data retention/purge rules to minimize risk.

Acceptance Criteria
Encrypted Autosave of In-Progress Forms
Given a user is filling a multi-step form in Kiosk Lock When the user pauses input for 2 seconds or navigates to a new field or step Then the current form state is autosaved to encrypted local storage within 200 ms And fields tagged "sensitive" in the form schema are not persisted in plaintext (omitted or null) And inspecting app storage shows no plaintext occurrences of any sensitive field values And autosave writes are debounced to at most once every 2 seconds per form And enabling Airplane Mode does not prevent autosave from completing
Restore After Crash to Last Safe Step
Given the app crashes during data entry with an autosaved draft present When the app is relaunched Then a "Restore in-progress entry?" prompt appears within 3 seconds And choosing Continue restores all non-sensitive fields and navigates to the last completed step boundary And any sensitive fields on the current step are blank and require re-entry And choosing Discard permanently deletes the draft from local storage and returns to the kiosk start screen And a recover/discard event is logged without sensitive payload
Auto-Relaunch into Kiosk Mode After OS Restart/Close
Given the device experiences an OS restart, low-battery shutdown, or the app is accidentally closed When power is restored or the OS finishes booting Then GiveCrew auto-launches within 5 seconds directly into Kiosk Lock with app switching blocked And the kiosk PIN/gesture remains unchanged and required to exit And, if a draft exists, the restore prompt is shown before any non-kiosk UI
Sensitive Field Redaction on Autosave and Restore
Given the form includes fields marked sensitive in the schema (e.g., payment details, government ID, passwords) When autosave occurs Then sensitive field values are not written to local storage in any recoverable form And on restore, sensitive fields render empty with masked placeholders and require re-entry And full-text search of storage files for known sensitive test values returns zero matches
Offline Queueing and Safe Retry of Completed Submissions
Given the user submits a form while the device is offline or the server is unreachable When submission occurs Then the submission is stored in an encrypted outbound queue with a unique client id and timestamp And the user sees a non-intrusive "Queued" confirmation within 1 second And the app retries delivery automatically on connectivity changes with exponential backoff up to 5 minutes And upon successful server acknowledgment, the queued item is removed and a receipt is marked as sent And if the server indicates a duplicate for the unique client id, the item is marked delivered without resubmitting the payload And the queue persists across app and OS restarts
Schema Migration on App Update with Data Retention/Purge Rules
Given an app update introduces a new local draft schema When the updated app launches with existing autosaved drafts present Then compatible drafts are migrated to the new schema with no data loss for non-sensitive fields And incompatible drafts are safely purged or partially restored per defined rules, with sensitive fields always purged And the user is informed via a non-blocking notice that a draft was migrated or discarded And the app does not crash or hang during migration; launch completes within 5 seconds And a migration log entry is recorded without sensitive payloads
Manual Discard Flow and Secure Purge
Given a restore prompt is shown for an in-progress draft When the user selects Discard and confirms Then all local storage artifacts for that draft (records, blobs, indices, keys) are securely deleted within 500 ms And subsequent storage scans cannot recover the discarded data And the kiosk flow returns to the start state ready for a new entry
Auto Relaunch on Boot and App Restart
"As a site lead, I want the app to come back to kiosk mode automatically after a reboot so that volunteers don’t need IT steps."
Description

Registers for system boot and app restart events to automatically start GiveCrew directly into kiosk mode without manual steps. Performs a preflight check for required OS settings (e.g., Guided Access/Lock Task) and charging state, showing clear guided prompts if prerequisites are missing. Bypasses onboarding and restores kiosk configuration so the station becomes ready within seconds after a power cycle or accidental close.

Acceptance Criteria
Device Reboots During Event
Given the device was previously running GiveCrew in kiosk mode with a saved configuration And the device powers off or reboots When the device finishes booting and the lock screen is unlocked Then GiveCrew auto-launches into kiosk mode within 10 seconds without the user selecting the app And no onboarding or login screens are shown And the kiosk PIN/gesture unlock remains enforced
App Force-Closed or Crashed
Given GiveCrew is in kiosk mode When the app is force-closed, swiped away, or crashes while foregrounded Then GiveCrew restarts automatically and returns to kiosk mode within 5 seconds And the previously active station profile and selected flow are restored And no onboarding or setup screens are shown
Missing OS Kiosk Setting Detected
Given the required OS kiosk capability (e.g., Guided Access or Lock Task Mode) is disabled or unavailable When GiveCrew attempts to auto-launch into kiosk mode Then a blocking preflight screen appears with a clear title, description, and an action to open the correct OS setting And the user cannot proceed to kiosk until the prerequisite is enabled And upon enabling the prerequisite and returning to the app, it automatically proceeds into kiosk mode without restarting
Device Not Charging at Station Setup
Given the battery level is below 20% and the device is not connected to power When GiveCrew auto-launches after boot or restart Then a preflight warning appears instructing the user to connect power and showing the current battery percentage And the warning provides actions to connect/open relevant system settings and to proceed And after dismissing the warning or connecting power, the app proceeds to kiosk mode within 2 seconds
Restore Last Kiosk Configuration
Given a station profile with saved kiosk configuration exists When GiveCrew auto-launches after boot or app restart Then the following settings are restored exactly as last saved: station profile, selected form/flow, UI language, theme/branding, kiosk PIN/gesture, and idle timeout And the initial screen matches the last configured starting screen for that station And any admin changes to configuration persist across relaunch
Onboarding Bypassed in Kiosk Relaunch
Given GiveCrew has completed onboarding previously on this device When the app auto-launches after boot or restart Then onboarding, tutorials, and account selection screens are not displayed And zero user taps are required after OS unlock for the station to be ready, provided prerequisites are satisfied And the first interactive view is the kiosk-ready screen for the configured flow
Idle Timeout & Attract Screen
"As an event organizer, I want the kiosk to reset and show a clear start screen after inactivity so that it’s always ready for the next person."
Description

Detects inactivity and auto-resets the current flow to a branded, high-contrast attract screen with clear calls to action (Sign Up, Donate, Take a Shift). Implements configurable idle timeout, screen dimming, and anti-burn-in behaviors. Ensures no PII is visible during idle and that the next interaction starts from a clean state. Optionally rotates Impact Board highlights to draw engagement while preserving kiosk security constraints.

Acceptance Criteria
Auto-Reset to Attract Screen After Inactivity
Given Kiosk Lock is enabled and any flow screen is in the foreground And no input events (touch, key, scroll) occur for the configured idle timeout T When T elapses Then the app navigates to the Attract Screen within 500 ms And all unsaved form inputs on the current screen are discarded And any active media capture is stopped And no PII is visible anywhere on screen Given any input event occurs before T elapses When the event is received Then the idle timer resets And no navigation to the Attract Screen occurs
Idle Timeout Configuration
Given an admin is authenticated in Kiosk Settings When they set Idle Timeout to X seconds between 15 and 600 (inclusive) and save Then the value persists to device storage and remote config And the running session uses X without app restart Given they attempt to set a value outside 15–600 When they save Then the setting is rejected with inline error "Idle timeout must be 15–600 seconds"
Screen Dimming and Anti-Burn-In
Given Idle Timeout is X and Dimming Offset is Y seconds (5–30, default 10) When inactivity duration reaches X − Y seconds Then the screen dims to 30% ±5% brightness (or an in-app dim overlay if OS prevents brightness control) And dimming is immediately removed on the next input event Given the Attract Screen is displayed When anti-burn-in is active Then the attract content shifts by 2–3% of width/height in a random direction every 30 ±5 seconds And no static element remains fixed in the same position for more than 60 seconds
PII Redaction and Clean State Guarantee
Given the idle reset has navigated to the Attract Screen When a user taps any CTA Then the selected flow starts with empty fields and no prefilled PII And the back action cannot return to any pre-reset screen state And any partial photos or attachments captured but not submitted are deleted
Attract Screen Content and Accessibility
Given the Attract Screen is displayed Then it shows organization branding and exactly three primary CTAs: "Sign Up", "Donate", "Take a Shift" And each CTA meets WCAG 2.1 AA contrast (>= 4.5:1) and minimum touch target 44x44 dp And the screen loads in under 500 ms on a cold start and under 200 ms from an in-app reset And all text is localized to the device language (fallback to English)
Impact Board Rotation With Security Constraints
Given Impact Board rotation is enabled in Kiosk Settings When the Attract Screen is displayed Then it cycles through Impact Board highlight cards every 10 ±2 seconds And only aggregate metrics are shown; no names, emails, phone numbers, or free-text notes are displayed And tapping Impact content does not navigate; only CTAs initiate flows And kiosk lock constraints remain enforced (no external links or app switching)
Return to Attract After App Resume or Restart
Given the device screen turns back on, the app resumes, or the app relaunches after an OS kill When the app comes to the foreground Then it shows the Attract Screen within 1 second And no PII from any prior session is visible And previously configured idle, dimming, and rotation settings are applied
Tamper Detection & Audit Logs
"As a program manager, I want tamper and health logs so that I can diagnose issues and prove data protection during events."
Description

Captures and securely stores non-PII logs of kiosk health and security events, including attempts to exit, failed PIN entries, unexpected app closures, OS dialog exposures, power loss, and lock prerequisite changes. Surfaces a simple on-device health indicator and an exportable report. Streams summarized telemetry to the admin dashboard for fleet oversight while respecting attendee privacy. Enables faster diagnosis during field issues and demonstrates due diligence in protecting data.

Acceptance Criteria
App Exit Attempt Logging
Given kiosk mode is enabled and a session is active When a user attempts to exit the app via home, back, recents, app switch, or system gesture Then the attempt is blocked and an audit entry is appended to the encrypted, append‑only local log with fields {event_type:"exit_attempt", method, outcome:"blocked", timestamp_utc, session_id, device_id_hash, app_version} And the entry contains no attendee PII or screen content And the entry is included in the next audit report export for the selected time range
Failed PIN Attempts Logging (Non‑PII)
Given a kiosk unlock PIN is configured and the device is in kiosk mode When an incorrect PIN is submitted Then an audit entry is appended with fields {event_type:"pin_fail", timestamp_utc, attempt_index_in_window, session_id, device_id_hash} And no entered digits, PIN length, or keypad pattern are stored And after 5 failed attempts within 5 minutes a {event_type:"pin_fail_threshold", count:5, window_minutes:5} entry is logged
Unexpected Closure/Crash Logging and Recovery
Given the kiosk is active When the app is terminated unexpectedly (crash, ANR, force‑stop, or OS reclaim) Then on next launch an audit entry is appended with fields {event_type:"unexpected_closure", reason, timestamp_utc, previous_uptime_s} And a follow‑up entry records the lock restoration attempt {event_type:"lock_restore_attempt", outcome, timestamp_utc} And the entries contain no attendee PII
Security Surface Exposure or Prerequisite Change Detected and Logged
Given the kiosk is active When an OS dialog or overlay not initiated by the app becomes visible (e.g., permission prompt, system update, low battery) Then an audit entry is appended with fields {event_type:"os_dialog_exposed", dialog_category, duration_ms, timestamp_utc} And dialog text or screenshots are not captured When a required lock prerequisite changes (e.g., required permission revoked, device admin/accessibility/overlay setting toggled) Then an audit entry is appended with fields {event_type:"prereq_changed", prereq_name, new_state, timestamp_utc} And both entries are included in the exportable audit report
Power Loss/Restart Logging and Continuity
Given the kiosk is active When the device experiences power loss or a battery‑drain shutdown Then on next launch audit entries are appended {event_type:"power_loss", timestamp_utc, last_known_battery} and {event_type:"device_restart", timestamp_utc} And the audit log’s hash‑chain integrity is verified and a {event_type:"audit_log_integrity", status:"ok"|"gap_detected"} entry is appended And no attendee PII is recorded
On‑Device Health Indicator Reflects Kiosk Health
Given the health indicator is visible on the Kiosk Lock status screen When no security events occur within the last 15 minutes and all prerequisites are satisfied Then the indicator shows Green with label "Secure" When 3 or more exit attempts or PIN failures occur within 5 minutes or an OS dialog remains visible longer than 5 seconds Then the indicator shows Amber with label "Attention" and the top reason When any prerequisite is disabled, 2 or more unexpected closures occur within 10 minutes, or a power‑loss event occurred in the last session Then the indicator shows Red with label "Action Required" and the top reason And tapping the indicator reveals the last 5 relevant events without PII
Audit Log Export and Privacy‑Preserving Telemetry Stream
Given a team member with device access opens Kiosk Lock settings When they select Export Audit Report and choose a time range Then a CSV and JSON report are generated with fields [timestamp_utc, event_type, attributes…] excluding PII, signed with a SHA‑256 checksum, and shared via the OS share sheet Given the device has network connectivity When audit events are recorded Then summarized telemetry (event counts per type per 5‑minute window, current health state, app/device versions, pseudonymous device_id) is transmitted via TLS to the admin dashboard within 2 minutes and queued for retry when offline And the telemetry payload excludes PII and raw event contents

LabelSpool

Offline label queue that formats sticky name badges with phonetic hints and partner attribution, then batch-prints automatically on reconnect. Each label can include a tiny QR that opens the volunteer’s profile or check-in code, speeding day-of operations without manual lookup.

Requirements

Offline Label Queue
"As a check-in volunteer, I want to queue and print name badges without internet so that lines keep moving at busy events."
Description

Provide an offline-first label job queue that persists locally on device with encryption. Each job stores volunteer identifier, template version, field payload (name, phonetic hint, partner tag), QR token, and target printer profile. Support enqueue during signup, check-in, and roster actions while offline. Expose job states (queued, printing, printed, failed) and queue length in UI. Implement idempotency via stable job hashes to prevent duplicates across retries and device restarts. Apply exponential backoff and resumable retries on reconnect or printer availability. Resolve data drift by applying latest profile data at print time unless the job is explicitly locked; surface conflicts for operator review. Tolerate app restarts, low-power modes, and intermittent connectivity without data loss. Sync minimal metadata (job id, status, timestamps) to server when online for auditing while avoiding PII leakage. Auto-purge printed/expired jobs after a configurable retention window.

Acceptance Criteria
Offline Enqueue from Signup, Check-in, and Roster
Given the device is offline and a user completes a signup with "Print label" selected When the action is submitted Then a label job is enqueued with fields: volunteerId, templateVersion, payload.name (required), payload.phoneticHint (optional), payload.partnerTag (optional), qrToken (required), printerProfileId (required), state=queued And the job appears in the queue UI within 2 seconds and the queue length increments by 1 Given the device is offline and a user checks in a volunteer with "Print label" selected When the action is submitted Then a label job with the same required fields is enqueued with state=queued and visible in the queue within 2 seconds Given the device is offline and a roster action triggers "Print label" When the action is submitted Then a label job with the same required fields is enqueued with state=queued and visible in the queue within 2 seconds
Encrypted Local Persistence and Resilience
Given N jobs are in the queue When the app is force-closed and relaunched Then all N jobs with their exact fields and states are restored with no loss or duplication Given N jobs are in the queue When the device enters low-power mode, reboots, or experiences intermittent connectivity Then all N jobs and their states persist and resume without data loss Given jobs are stored locally When storage is inspected outside the app context Then PII fields (payload.name, payload.phoneticHint, payload.partnerTag, qrToken) are not readable in plaintext (data is encrypted at rest)
Queue State Machine and UI Visibility
Given a job is queued When printing starts Then its state transitions to printing and the UI reflects the change within 2 seconds Given a printing job completes successfully When the printer confirms completion Then its state transitions to printed, printedAt is timestamped, and the queue length reflects removal from active count Given a printing job fails (e.g., printer error or timeout) When the failure is recorded Then its state transitions to failed with an error code and next retry time visible in the UI Given multiple jobs exist in states queued, printing, and failed When the queue screen is viewed Then the displayed queue length equals the count of jobs with state in {queued, printing, failed}
Idempotency via Stable Job Hash
Given a job with hash H exists in state queued, printing, or failed (pending retry) When an identical job (same volunteerId, templateVersion, payload, qrToken, printerProfileId) is enqueued Then no new job is added and the existing job with hash H is retained Given the app restarts while jobs exist When the queue is rehydrated and the scheduler resumes Then no additional duplicate jobs are created; existing jobIds (hashes) are preserved Given transient acknowledgment loss causes a retry for job hash H When printer acknowledgments are reconciled Then only one physical label is produced for H (no duplicate print across automatic retries or restarts)
Exponential Backoff and Resumable Retries
Given a queued job targets an unavailable printer or missing connectivity When automatic retries are scheduled Then attempts follow exponential backoff with jitter starting at ~1s and doubling up to a max interval of 5 minutes, with 10–20% randomization Given connectivity or the target printer becomes available When the next scheduled retry time arrives or an availability event is detected Then the job automatically transitions to printing without user action Given the app is backgrounded and later foregrounded When the scheduler resumes Then pending retry timers are restored without resetting the backoff progression
Data Drift Resolution and Locking
Given a queued job is unlocked and the volunteer profile has been updated since enqueue When the job is about to print Then the latest profile data is applied to the label payload automatically Given applying latest data changes any printed-visible field (e.g., name or partner tag) When the operator views the job detail or print confirmation Then a conflict notice presents options "Use Latest" or "Use Original" and the selected choice is applied and recorded in job metadata Given a job is explicitly locked by the operator When it prints Then the original payload from enqueue time is used regardless of subsequent profile changes
Audit-Safe Sync and Retention Purge
Given the device is online When a job is created or its state changes Then the server is synced with only minimal metadata: jobId (hash), state, createdAt, updatedAt, printedAt (if any), device/app instance id, and printerProfileId, and no PII fields (payload.name, payload.phoneticHint, payload.partnerTag, qrToken) are sent Given the retention window is configured to T hours (default 72) When a job in state printed or failed exceeds T hours since printedAt or updatedAt Then the job is automatically purged from local storage, removing all PII while leaving previously synced metadata on the server Given purging has occurred When the queue UI is refreshed Then purged jobs no longer appear and local storage usage reflects the removal
Dynamic Badge Templates & Layout
"As an organizer, I want a consistent badge layout with key fields and a QR so that badges are readable and on-brand across all events."
Description

Implement a printer-aware template system for sticky name badges that supports: large first name, last initial, optional role label, phonetic hint styled subtly, partner attribution (logo or short tag), and a compact QR placed consistently. Handle printer DPI, label sizes (e.g., 1x2–1x3 in / 62mm), margins, rotation, and bleed. Provide event/org-level theme presets, template versioning, and on-device preview with a test print flow. Ensure robust text overflow handling, wrapping/truncation rules, RTL support, diacritics, and accessibility contrast targets. Cache partner logos for offline use with dimension constraints and fallbacks to text if assets are missing. Guarantee consistent output across iOS/Android by normalizing rendering to raster/command languages per printer profile.

Acceptance Criteria
Core Badge Layout and QR Placement
Given a volunteer with first name, last initial, optional role, optional phonetic hint, and partner attribution And a selected badge template for a 1x3 in or 1x2 in or 62mm label When the template renders for print using a 300 or 203 DPI printer profile Then the first name is rendered in the primary style with a text height at least 1.8x the role label text height and not less than 18 pt And the last initial renders on the same baseline as the first name, capped at 70% of first-name cap height, with a trailing period And the role label (if present) renders below the name, max 2 lines, with ellipsis if overflow after the second line And the phonetic hint (if present) renders in the designated subtle style and does not exceed 80% of the role label size And a QR code renders in the template’s reserved corner zone with a size between 10–14 mm and a minimum 2 mm margin to label edges And no elements overlap and all elements remain within the template’s safe area
Printer Profiles, Sizes, Margins, Rotation, and Bleed
Given printer profiles for 203 DPI and 300 DPI devices and label sizes 1x2 in, 1x3 in, and 62mm continuous When a test template is rendered and printed via each profile Then the printed content aligns to the safe area with combined margin and bleed of at least 2 mm on all sides And measured element positions are within ±0.5 mm of the template’s expected coordinates And user-selected rotation (0/90/180/270) is applied identically in preview and on printed output And continuous 62mm labels auto-rotate for best fit when necessary without clipping any content
Text Overflow, Wrapping, and Truncation Rules
Given names, roles, and partner tags that exceed available width or height When the template renders Then the name keeps the first name and last initial visible; excess characters are truncated with a single ellipsis And the role label wraps up to 2 lines and truncates with ellipsis at the end of line 2 And phonetic hints hide automatically if showing them would cause any overlap or push content outside the safe area And no glyphs cross label bounds; no text overlaps other elements
Internationalization: RTL Scripts and Diacritics
Given a volunteer whose name and role are in an RTL language (e.g., Arabic or Hebrew) and/or include diacritics When the template renders Then the name and role text direction follows RTL with correct glyph shaping and joining And punctuation for last initial is placed according to the text direction rules And diacritics are preserved without clipping or collision at all font sizes used in the template And if the template includes mixed LTR/RTL content, numeric tokens and QR remain LTR and aligned correctly
Accessibility: Contrast and Minimum Legibility
Given any theme preset and background color or image When the template renders text elements Then all non-decorative text meets WCAG 2.2 AA contrast (≥4.5:1 for normal text, ≥3:1 for large text) And the first name renders at a minimum of 18 pt, the role label at a minimum of 9 pt, and phonetic hint at a minimum of 7 pt And printed output on supported thermal printers uses monochrome dithering that preserves legibility with no text strokes broken beyond 1 pixel gaps
Partner Attribution: Logo Caching, Sizing, and Fallback
Given a partner logo URL and a short attribution tag When the asset is downloaded successfully at least once Then it is cached for offline use and persists across app restarts And at render time the logo scales to fit within its reserved bounding box (max 14×14 mm) preserving aspect ratio, with no upscaling beyond 2× of the source And if the logo is missing, corrupted, or would render smaller than 4 mm on any side, the system replaces it with the short attribution tag text styled per template And partner attribution never overlaps the QR or name area and remains within the safe area
Theme Presets, Versioning, Preview Accuracy, and Cross-Platform Consistency
Given org-level default and event-level override theme presets When an event selects a preset and publishes a new template version Then the new version is assigned a monotonically increasing version ID and stored immutably And labels enqueued before publication keep their original template version; labels enqueued after use the new version And the on-device preview uses the same rendering pipeline as printing; a test print of a previewed label matches the preview within ±0.5 mm on paper And the same label rendered and printed from iOS and Android with the same printer profile produces raster/command output that yields a pixel-by-pixel visual difference of less than 1% area
Phonetic Hint Capture & Generation
"As a volunteer, I want my name’s pronunciation printed on my badge so that people address me correctly and I feel welcome."
Description

Add a "Pronounce as" field to volunteer profiles and check-in flows with inline edit at label time. Provide optional, offline-friendly suggestions using locale-aware transliteration rules; never auto-apply without user confirmation. Enforce a concise, human-readable format (e.g., DEE-ahn-dray) with character limits and guidance. Store per-locale hints and sync changes when online. Include the hint in the label payload if present and style according to template. Respect privacy and opt-out settings; do not expose hints in public endpoints or exports without permission.

Acceptance Criteria
Profile & Check-in 'Pronounce as' Field with Inline Label Edit
- Given a volunteer profile is opened, When the user taps Edit, Then a "Pronounce as" input is visible and editable. - Given the mobile check-in flow is active, When a volunteer is selected, Then a "Pronounce as" input is displayed inline with any existing value prefilled. - Given a label preview in LabelSpool, When the user taps the phonetic hint area, Then an inline editor opens to modify the hint prior to queuing. - Given the user saves an edit, When the device is offline, Then the value is persisted locally within 1s and reflected immediately in the current label preview. - Given no hint exists, When viewing the label preview, Then placeholder guidance is shown only in-app and is never printed. - Given the field is empty at queue time, When the label payload is generated, Then no phonetic hint field/value is included.
Offline Locale-Aware Phonetic Suggestions with Explicit Apply
- Given the device is offline and the app locale is set, When the user focuses the "Pronounce as" input for a name, Then up to 3 locale-aware suggestion chips appear within 1s labeled as Suggested (if rules match). - Given suggestions are displayed, When the user does not tap a suggestion or Apply, Then the field value remains unchanged (no auto-apply). - Given a suggestion is tapped, When the edit is saved, Then the field is populated with the suggestion and marked as user-confirmed in local state. - Given the device returns online, When updated suggestion rules are available, Then suggestions refresh on next focus but still require explicit user action to apply. - Given no matching rules exist, When the input is focused, Then no suggestions are shown and no error message is displayed.
Format Validation and Guidance for Phonetic Hints
- Given the user enters a value, When saving or queuing a label, Then validation permits only letters (A–Z, case-insensitive), spaces, hyphens (-), and apostrophes ('), disallows leading/trailing or consecutive separators, and enforces a maximum length of 32 characters. - Given validation fails, When the user attempts to save or queue, Then an inline error message appears within 300ms and the action is blocked. - Given validation passes, When saving, Then the app trims surrounding whitespace but does not auto-change letter casing or insert separators. - Given the input is empty, When focusing the field, Then helper text is shown: "Use hyphens to split syllables; use CAPS for stress (e.g., DEE-ahn-dray)."
Per-Locale Storage and Sync on Reconnect
- Given the device is offline, When a user saves a hint under app locale en-US, Then it is stored as pronounceAs["en-US"] for the volunteer and flagged Pending Sync. - Given connectivity is restored, When background sync runs, Then all Pending Sync hints are uploaded within 60s. - Given the server has a newer updatedAt for the same locale, When a conflict is detected, Then server value wins and the device shows a non-blocking conflict notice with an option to re-apply the local value. - Given multiple locale hints exist (e.g., en-US and es-MX), When syncing, Then both are preserved without overwriting each other. - Given a second device signs in, When it syncs after the update, Then it receives the latest per-locale hints for the volunteer.
Label Payload Inclusion and Template Styling
- Given a valid hint exists for the label's target locale, When a label is queued offline via LabelSpool, Then the label payload includes pronounceAs set to that hint. - Given the user edits the hint inline at label time, When the label is queued, Then the payload uses the edited value. - Given the printer reconnects and batch printing starts, When labels render, Then the hint appears styled per the active template (e.g., smaller italic under the name) and text does not overflow its bounding box. - Given no hint exists for the target locale, When generating the payload, Then the hint field is omitted and the template collapses the hint area without leaving an empty line. - Given the template specifies a locale L, When both L and a default hint exist, Then L is used; else fallback to default; else omit. - Given QR codes are generated, When labels print, Then QR content remains unchanged and does not include the phonetic hint.
Privacy and Opt-out Enforcement for Phonetic Hints
- Given the organization enables "Hide phonetic hints in external outputs" or the volunteer has opted out, When exporting CSV or generating public endpoints, Then pronounceAs is excluded from files and responses. - Given an unauthenticated or non-privileged API client, When requesting volunteer profiles, Then pronounceAs is omitted or null regardless of stored value. - Given an internal user with appropriate permissions, When viewing labels and internal reports, Then pronounceAs is visible unless the individual volunteer has opted out. - Given a label is prepared for an opted-out volunteer, When printing, Then the phonetic hint is not displayed and is not included in the label payload.
Partner Attribution & Branding Rules
"As a coalition coordinator, I want partner attribution on badges so that we can recognize partners and route volunteers appropriately on event day."
Description

Derive partner attribution from referral source, signup code, assignment, or manual override according to event-level rules with clear priority order. Display a short partner tag or logo on the badge per template constraints. Validate and cache logos for offline printing; convert to monochrome when required for legibility. Provide admin controls to map sources to partners, approve visibility, and define sensitive partners that must not be printed. Fall back to text when assets are missing, and ensure attribution is included in audit metadata without exposing sensitive content publicly.

Acceptance Criteria
Attribution Priority Resolution
Given the event-level priority order is Manual Override > Assignment > Signup Code > Referral Source When partner attribution is computed for a volunteer with multiple sources Then the selected partner follows that priority deterministically and consistently offline and online Given only one mapped source is present When attribution is computed Then that partner is selected Given two sources resolve to the same partner When attribution is computed Then the common partner is selected and no conflict is logged Given no mapped sources and no override When attribution is computed Then partner is null and fallback rendering rules apply Then selected partner_id and resolution_path are written to audit metadata for each label
Badge Template Rendering with Tag/Logo
Given the selected partner has Approved for Printing = true and a valid logo asset When rendering the badge for the active template Then the logo is placed within the partner box, scaled to fit without clipping, and aspect ratio preserved Given the printer or template requires monochrome When rendering the logo Then the logo is converted to monochrome and achieves a minimum contrast threshold against background; if threshold is not met, fallback to text tag is used Given the partner has a short tag When logo rendering is disabled, fails validation, or is suppressed Then the tag is rendered per template constraints (max length, truncation/ellipsis rules) without overlapping QR or other fields Given all elements are composed (name, phonetics, QR, partner) When rendering on the reference device Then total render time per label is <= 250 ms and layout validation reports no overlaps or overflow
Offline Asset Validation and Caching
Given a partner logo has passed validation (type, dimensions, transparency) while online When the device is offline Then the cached, pre-rasterized asset is used for rendering without network calls Given a cached asset is older than the configured TTL (default 30 days) or fails checksum validation When rendering offline Then printing falls back to the partner text tag and a cache refresh job is queued for next reconnect Given no cached asset exists for the selected partner When rendering offline Then the label prints with the partner text tag and no blocking errors occur Given connectivity is restored When cache refresh runs Then assets are revalidated and updated atomically without interrupting queued prints
Admin Mapping and Visibility Controls
Given an admin is configuring event-level rules When mapping referral domains, signup code prefixes, or assignment teams to a partner Then the mapping saves with validation and appears in the rule list with its match scope Given overlapping mappings exist When saving rules Then the UI requires a priority order and shows the effective preview; the saved order is used by attribution Given a partner has Approved for Printing toggled off at the event level When that partner is selected by attribution Then branding on the badge is suppressed and suppression_reason is recorded in audit metadata Given an admin uploads a logo asset for a partner When the file type, size, or dimensions are invalid Then the upload is rejected with a descriptive error and no cache entry is created
Sensitive Partner Suppression and Audit Safety
Given a partner is marked Sensitive When a label is rendered Then no logo or tag for that partner is printed and no placeholder text reveals the partner Then audit metadata includes partner_id and suppression_reason but excludes partner name, tag text, or asset URIs from public payloads, exports, and QR content Given a non-sensitive partner with Approved for Printing = false at the event level When a label is rendered Then branding is suppressed and audit records event-level suppression without exposing sensitive content Given a public export or Impact Board sync is generated When sensitive partner data is present Then partner branding fields are redacted while internal audit retains full attribution
Manual Override with Audit Trail
Given an event lead applies a manual partner override at volunteer, batch, or event scope When labels are queued Then the override partner is used and audit logs actor_id, timestamp, scope, and expiration Given an override expires or is cleared When subsequent labels are queued Then attribution reverts to computed rules and the audit records the change Given conflicting overrides exist at multiple scopes When rendering a label Then the most specific scope wins (volunteer > batch > event) and the resolution_path is recorded in audit metadata
Secure QR Deep Links
"As a staffer, I want a small QR on each badge that opens a volunteer’s profile or check-in so that I can retrieve records instantly without manual search."
Description

Generate compact QR codes that resolve to a volunteer’s profile or check-in action via a short HTTPS URL with a signed, scope-limited token (e.g., HMAC/JWT). Tokens must encode minimal identifiers, enforce TTLs, and support revocation. Implement deep link handling to open the GiveCrew app when installed or a mobile web fallback with appropriate auth/permissions. Pre-provision tokens for expected rosters to enable offline QR generation; rotate signing keys per org/event. Prevent enumeration by using non-sequential identifiers and rate limiting when online. Record scan events for audit and impact metrics when connectivity is available.

Acceptance Criteria
Compact Short-URL QR Generation
Given an organization with a configured HTTPS short-link domain When a label is generated for a volunteer via LabelSpool Then the QR encodes an HTTPS URL on the configured domain And the total URL length is <= 60 characters And the URL contains no PII in its path or query parameters And the URL includes only a single opaque, signed token as a path segment or parameter And the QR encodes at version <= 10 with error correction level M or higher
Scope-Limited Token With TTL and Minimal Claims
Given a token issued for scope "checkin" or "profile" Then the token payload includes only org_id, optional event_id, opaque volunteer_ref, scope, iat, exp, jti, and kid And the token contains no name, email, phone, or address fields And exp - iat <= 24 hours for scope "checkin" and <= 7 days for scope "profile" And the token signature validates against the current org/event signing key matching kid When current time exceeds exp Then resolving the URL returns 401 Expired and the client displays "Link expired"
Deep Link App Handling With Web Fallback
Given a mobile device with the GiveCrew app installed When the QR URL is opened Then the OS routes into the GiveCrew app via deep link matching the token scope within 2 seconds And the app enforces permissions for the requested action, showing 403 Access Denied if unauthorized Given a mobile device without the app installed When the QR URL is opened Then the user is taken to the mobile web fallback over HTTPS And if unauthenticated, the user is prompted to sign in and then redirected to the intended action And no third-party requests include the token in the Referer header
Offline Labels Using Pre-Provisioned Tokens
Given an event roster synced within the last 24 hours and the device is offline When labels are generated for roster entries with pre-provisioned tokens Then QR codes are rendered and printed without any network calls And 100% of those labels include valid tokens that verify locally And labels for entries lacking pre-provisioned tokens are queued with status "Waiting for token" When connectivity is restored Then queued labels auto-generate tokens and print within 60 seconds
Per-Org/Event Key Rotation and Revocation
Given an organization rotates its signing key and publishes a new kid When new tokens are issued Then they are signed with the new key and validate successfully And tokens signed with the previous key continue to validate for a grace period <= 10 minutes When a key is marked revoked Then tokens bearing that kid are rejected immediately with 401 Revoked And rotation and revocation events are recorded with timestamp, org_id, and kid
Non-Enumerability and Online Rate Limiting
Given short-link endpoints receive invalid or random tokens When more than 20 invalid requests originate from the same IP within 60 seconds Then further requests are throttled for 5 minutes with HTTP 429 and a Retry-After header And responses for invalid tokens use a uniform status/body size to avoid oracle leakage And token identifiers are non-sequential and provide >= 128 bits of entropy (verified by automated tests)
Scan Event Auditing and Metrics Sync
Given a valid scan resolves while online When the action completes Then an audit event is recorded containing event_id, org_id, volunteer_ref, scope, outcome, device_id, ts (UTC), and actor_id (if authenticated) And the event is available to analytics within 5 seconds Given a scan occurs while offline in the app When connectivity is restored Then queued events sync within 60 seconds in original order with idempotent deduplication by (jti, device_id, ts) And queued events are encrypted at rest until synced
Auto Batch Print & Printer Discovery
"As an event lead, I want queued labels to auto‑print when devices reconnect so that we catch up quickly without extra taps or rework."
Description

Detect printer and network connectivity changes and automatically dispatch queued label jobs on reconnect in FIFO order, grouped by printer/template for throughput. Support Bluetooth and Wi‑Fi printers commonly used for badges (e.g., Brother QL, Zebra ZD, DYMO), with per-printer profiles (DPI, media size, command language such as ESC/P, ZPL, AirPrint). Provide device-level pairing, per-event default selection, and quick switching. Validate media type/orientation; surface actionable errors (paper out, jam, low battery) and allow pause/resume. Mark jobs printed based on printer acknowledgments to avoid duplicates and implement device/server leases to prevent double-prints across multiple devices. Enable quiet background printing with progress and reprint options; target <2s first-label time on supported printers and sustain 200 labels/hour under load.

Acceptance Criteria
Automatic Dispatch on Reconnect (FIFO, Grouped by Printer/Template)
Given the device has 45 queued label jobs across multiple printers and templates while offline And at least one supported printer (e.g., Brother QL-820NWB via Bluetooth and Zebra ZD420 via Wi‑Fi) is configured When network connectivity is restored and printers become reachable Then the queue is partitioned by printer and template and dispatched in FIFO order within each partition And no job enqueued earlier within a partition prints after a later job in the same partition And the first label from the first ready partition starts printing within 2 seconds of reconnect on supported printers And partitions targeting unavailable printers remain queued with a clear status while other partitions dispatch
Printer Discovery, Pairing, and Profiles (Bluetooth & Wi‑Fi; ESC/P, ZPL, AirPrint)
Given compatible printers (Brother QL-820NWB, Zebra ZD420, DYMO LabelWriter Wireless) are powered on When discovery is initiated over Bluetooth and the local Wi‑Fi network Then each printer appears in the list with model, connectivity type, and signal/availability within 5 seconds When a user selects a printer and completes pairing/authorization Then device-level pairing is established and verified by a successful test print And a per-printer profile is created storing DPI, media size, command language (ESC/P, ZPL, AirPrint), and connection parameters When printing via each profile Then the generated commands match the stored command language and the printed label dimensions match the configured media within ±1 mm And the profile persists across app relaunch and is reusable by future jobs
Per-Event Default Printer and Quick Switching
Given an active event is selected in the app When the user sets Printer A as the default for this event Then new label jobs for the event route to Printer A without additional prompts And the default persists across app relaunches for the same event When the user invokes quick switch from the print UI Then the printer can be changed in 2 taps or fewer and in under 2 seconds And subsequent jobs use the newly selected printer while already-dispatched jobs are unaffected
Media Type and Orientation Validation
Given a label job specifies a template requiring 62 mm continuous media in landscape orientation And the selected printer reports currently loaded media and orientation When the installed media type or orientation does not match the job requirements Then printing is blocked and the app surfaces a specific actionable error (e.g., "Expected 62 mm continuous (landscape); detected 62×100 die-cut (portrait)") And the user is provided actions to Retry detection after correcting media or Change printer When the correct media and orientation are confirmed by the printer Then the job becomes printable without recreating the job
Actionable Error Handling with Pause/Resume
Given a batch of labels is actively printing When a recoverable condition occurs (paper out, jam, head open, low battery) Then the app pauses the affected printer's queue within one label and displays a clear error with icon and remediation steps And the user can choose Resume, Pause Queue, or Cancel Current Job When the condition is resolved and Resume is selected Then printing continues from the next unprinted label with no duplicates When Cancel Current Job is selected Then only the current job is canceled and the remainder of the queue remains intact
Acknowledgments, Deduplication, and Cross-Device Leases
Given two devices are signed into the same event and connected to the same printer model When both are online with access to the shared print queue Then exactly one device acquires a lease for a given printer/template partition and is allowed to dispatch jobs And the other device remains in a standby state for that partition When a job is sent to the printer Then it is marked Printed only upon receiving a positive printer acknowledgment within the protocol (status/OK) And if no acknowledgment is received within 10 seconds, the job remains Pending and at most one idempotent retry is attempted And no job prints more than once even under race conditions or reconnects And Reprint creates a new job linked to the original with a distinct ID
Background Printing Performance and Throughput
Given 300 label jobs are queued for a supported printer with a valid profile When printing begins from the foreground Then time to first printed label is under 2.0 seconds at the 95th percentile on reference devices for supported printers And sustained throughput is at least 200 labels/hour over a continuous 30‑minute run with 0% duplicate rate When the app is backgrounded during an active batch Then printing continues quietly with a persistent progress indicator and the ability to cancel or reprint upon return And if the OS suspends activity, the queue resumes within 2 seconds when the app returns to foreground with accurate progress

QuickBrand

One-tap theming that applies event name, sponsor logos, and contrast-checked colors to the kiosk screen and printable table-tent with a snapshot of rotating QRs. Looks legit in seconds, boosts trust and conversions, and keeps coalitions compliant with brand guidelines—even without Wi‑Fi.

Requirements

One-Tap Theme Apply
"As a field organizer, I want to apply our event’s branding to the kiosk and table-tent in one tap so that I can set up quickly and look professional."
Description

Applies event name, approved colors, sponsor lockup, and typography to the GiveCrew Kiosk screen and printable table-tent in a single action. Pulls inputs from the Event record and a selected Brand Pack, resolves defaults, and updates UI components (buttons, headers, form fields) and print placeholders. Persists a Theme instance tied to the event with versioning and revert-to-default. Operates without network when assets are cached, ensuring consistent look-and-feel across devices.

Acceptance Criteria
One-Tap Applies Brand Pack and Event Meta to Kiosk UI
Given an Event has a selected approved Brand Pack and required fields (event name) populated And the Kiosk is on the Theme screen with no pending edits When the user taps "Apply Theme" Then the header, primary buttons, secondary buttons, form field labels, inputs, and background update to the Brand Pack colors and typography And the event name renders in the kiosk header And all UI updates complete within 2 seconds when assets are cached, or within 5 seconds when online And a success toast "Theme applied" is displayed
Applies Theme to Printable Table-Tent with QR Snapshot
Given an Event has a selected Brand Pack and at least one active signup QR endpoint When the user taps "Apply Theme" Then a print-ready table-tent asset is generated locally using the Brand Pack typography and colors and the Event name And the asset includes a static snapshot image of the current rotating QR that decodes to the event’s signup URL And the asset is produced at print quality (vector text, QR raster >= 300 DPI) And generation completes within 3 seconds when assets are cached And the table-tent visual style matches the kiosk theme tokens (colors and fonts)
Offline Apply When Brand Assets Are Cached
Given the device is offline And the selected Brand Pack’s required assets (colors, fonts, sponsor lockup) are cached locally When the user taps "Apply Theme" Then the theme is applied without any network calls And the kiosk UI and table-tent asset are updated using cached assets And if a required asset is missing, the system substitutes the default asset and displays an informational message "Some brand assets unavailable offline"
Automatic Contrast Check and Accessible Color Selection
Given a Brand Pack with provided primary/secondary colors When the user taps "Apply Theme" Then the system calculates contrast ratios for all text/background pairs in headers, buttons, and form fields And any pair below WCAG 2.1 AA thresholds (4.5:1 for body text, 3:1 for large text/icons) is automatically adjusted using pack fallbacks or algorithmic tweaks And the applied colors meet or exceed the thresholds across all UI components And the Theme instance records whether contrast adjustments were applied
Sponsor Lockup Placement and Safe-Area Compliance
Given a Brand Pack with sponsor lockup assets (one or more logos) When the user taps "Apply Theme" Then sponsor logos render in the designated sponsor area on the kiosk and table-tent And logos preserve aspect ratio, are fully visible, and do not overlap each other or interactive UI elements And sponsor area respects safe margins on common device aspect ratios (16:9, 3:2, 4:3) without clipping And raster logos are not upscaled beyond their native resolution
Theme Persistence with Versioning and Revert to Default
Given an Event and a selected Brand Pack When the user taps "Apply Theme" Then a Theme instance is persisted with a unique version identifier, timestamp, user ID, event ID, and brand pack reference And reopening the event after app restart loads the same Theme instance and renders identically When the user taps "Revert to Default" Then the kiosk and table-tent revert to the system default theme And a new Theme version is created noting the revert action
Cross-Device Consistency of Applied Theme
Given Device A has applied a Theme to Event X and Device B opens Event X with the Theme instance available When both devices display the kiosk screen Then both render identical theme tokens (hex color values, font family/style names, and sponsor lockup order) And visual alignment differences do not exceed 1 px due to resolution scaling And colors and typography match exactly across devices
Offline Brand Pack Caching & Sync
"As a volunteer lead who works in low-connectivity venues, I want brand assets available offline so that I can theme the kiosk without Wi‑Fi."
Description

Downloads and securely caches brand assets (logos in SVG/PNG, color tokens, fonts, layout rules) on the device for offline use. Manages versioning, checksum validation, and automatic invalidation of stale assets. Performs delta synchronization upon reconnection and gracefully degrades to embedded safe defaults if assets are missing. Ensures all QuickBrand actions, including theme application and print export, function without Wi‑Fi.

Acceptance Criteria
Offline QuickBrand Theme Application
Given the device is offline and a valid brand pack (logos, color tokens, fonts, layout rules) is cached and verified When the user taps QuickBrand to apply the theme on a kiosk screen Then the theme applies within 2 seconds using only cached assets And all UI elements render the correct logos, fonts, and colors per cached layout rules And no network requests are attempted during application And an audit log entry records timestamp, user, and pack version used
Brand Pack Download and Integrity Verification
Given the device is online and a brand pack manifest with version IDs and SHA-256 checksums is available When the app downloads each asset referenced by the manifest Then each asset’s checksum matches the manifest value before being stored And assets with mismatched checksums are discarded and retried up to 3 times with exponential backoff And the download is marked failed if any asset cannot be verified And no unverified assets are stored or used by QuickBrand
Versioning and Stale Asset Invalidation
Given a cached brand pack version vN exists When a newer manifest version vN+1 is fetched Then assets whose content hashes differ between vN and vN+1 are marked stale immediately and no longer served And vN assets continue to be used until vN+1 assets are fully downloaded and verified And after verification, stale vN assets are purged from active cache within 1 second while unchanged assets are retained And the current active pack version is updated atomically to vN+1
Delta Synchronization After Reconnection
Given the device had been offline with cached pack vN and reconnects to a network When the app detects a newer manifest vN+1 Then it requests only the manifest and the assets whose hashes differ from vN (no re-download of unchanged assets) And total HTTP requests equal 1 (manifest) + number of changed assets And QuickBrand remains usable without visual regressions during the sync And the delta sync completes within 30 seconds for up to 20 changed assets averaging 200 KB on a 5 Mbps connection
Graceful Degradation to Safe Defaults
Given required brand assets are missing, corrupted, or invalidated and no network is available When the user applies a theme or initiates print export Then the app falls back to embedded safe defaults (placeholder logo, system font, WCAG AA–compliant color palette, default layout) And the operation completes without crash or blocking error And the resulting UI or export remains legible and functional And an audit log records that defaults were used due to unavailable assets
Offline Print Export With QR Snapshot
Given the device is offline and a valid brand pack and latest QR rotation snapshot are cached When the user generates the printable table-tent Then the export is produced entirely offline as a PDF for Letter and A4 presets at 300 DPI And it embeds cached logos, fonts, colors, layout rules, and the QR snapshot And the output matches template margins and element positions within ≤1 mm variance And the export completes within 5 seconds without any network requests
Secure At-Rest Caching and Access Control
Given brand assets are cached on the device When storage is inspected via platform tools and during app runtime Then assets are encrypted at rest with keys protected by the OS keystore/Keychain And cached files are only readable within the app sandbox (not by other apps or users) And logging out or clearing app data purges all cached assets and encryption keys And reinstalling the app generates new keys and does not restore prior cached assets
Automatic Contrast & Legibility Checks
"As an accessibility champion, I want the system to check and auto-fix contrast issues so that our materials are readable for everyone."
Description

Evaluates themed UI and print elements against WCAG 2.1 AA contrast thresholds in real time and on export. Flags violations and auto-adjusts color pairs, overlays, or stroke outlines to meet minimum ratios while staying within brand rules. Supports high-contrast toggle, generates an accessibility report stored with the Theme instance, and applies fixes consistently to kiosk screens and table-tent PDFs.

Acceptance Criteria
Real-Time Contrast Checks on Theme Apply
Given a user applies or edits a theme (colors, background image, sponsor logo) in QuickBrand When the preview renders on the kiosk and table-tent views Then every text and icon element has a measured contrast ratio >= 4.5:1 (normal text) or >= 3:1 (large text/icons) per WCAG 2.1 AA And each evaluated element displays a pass/fail indicator with the measured ratio And the evaluation completes and updates indicators within 150 ms for <= 50 elements and within 400 ms for <= 200 elements
Auto-Adjustments Within Brand Constraints
Given a contrast violation is detected for any element When Auto-adjust is enabled for the theme Then the system changes only allowed properties per brand rules (palette substitution, overlay opacity 0.20–0.60, text/icon stroke 1–3 px) And brand-locked colors are not altered; overlays or strokes are applied instead And after adjustment, all affected pairs meet the required ratios per WCAG 2.1 AA And if a violation cannot be resolved within brand constraints, it remains flagged as Unresolved with element ID and measured ratio
High-Contrast Toggle Compliance
Given the operator toggles High Contrast mode on the kiosk When High Contrast mode is active Then all text elements meet >= 7:1 (normal) and >= 4.5:1 (large/icons) contrast ratios And the high-contrast theme applies immediately across all kiosk screens and the print preview And the High Contrast state persists for the current session and resets on session end And a visible High Contrast indicator is present in the operator UI
Cross-Channel Consistency (Kiosk UI & Table-Tent PDF)
Given a theme with auto-adjustments is active When exporting the table-tent PDF Then the exported PDF uses the same final color values, overlays, and stroke parameters as the kiosk UI And contrast checks are re-evaluated on the export and all elements pass WCAG 2.1 AA And exported asset metadata records the color hex values used for each element And no element that passes in the UI fails in the PDF
Accessibility Report Generation & Storage
Given a theme is applied or exported When the Accessibility Report is generated Then the report includes theme ID, version, timestamp, user ID, per-element before/after colors, measured ratios, pass/fail status, and adjustments applied And the report is stored with the Theme instance and retrievable via UI and API by theme ID And the system retains the 10 most recent reports per theme with incrementing version numbers And the report can be downloaded as JSON and PDF
Offline Operation for Checks and Exports
Given the device has no network connectivity When a user applies a theme, runs contrast checks, or exports a table-tent Then all checks and auto-adjustments execute locally without network calls And the export succeeds to local storage with a Pending Sync flag And reports and exports queued while offline sync automatically when connectivity is restored And the UI shows a non-blocking Offline indicator instead of an error
Export Preflight and Policy Handling
Given unresolved contrast violations remain after permitted auto-adjustments When the user attempts to export the table-tent PDF Then an Export Preflight lists each violation with element ID, foreground/background colors, and measured ratio And export is blocked until all violations are resolved or org setting "Allow export with violations" is enabled And if export proceeds with violations, the Accessibility Report records them and the PDF metadata includes an AccessibilityWarningsPresent=true flag And resolving violations updates the preflight in real time and enables export when all pass
Sponsor Logo Manager & Layout Rules
"As a coalition coordinator, I want to manage sponsor logos with placement rules so that we stay compliant with agreements and look consistent."
Description

Enables selection and upload of multiple sponsor logos with tiering (title, supporting, community) and defines placement zones, alignment, safe-area padding, and minimum sizes. Auto-orders and scales logos while preserving aspect ratios, enforces brand do-not rules (e.g., no recoloring), and supports monochrome variants when required for contrast. Integrates logos into kiosk header/footer and print template consistently across sizes.

Acceptance Criteria
Bulk Upload and Tier Assignment
Given the organizer is in QuickBrand > Sponsor Logo Manager When they upload 1–20 logo files in a single action (SVG or PNG with transparency) Then all files are ingested without error and the original binaries are stored unchanged And a thumbnail is generated for each within 2 seconds per 5 logos Given logos are uploaded When the organizer assigns each to a tier (Title, Supporting, Community) Then the assignment persists across sessions and devices and is reflected in preview within 1 second Given an identical file (hash match) is re-uploaded When the organizer confirms or cancels replacement Then the system prevents duplicate rendering by default and records the latest version only if confirmed Given a corrupt or unsupported file is uploaded When validation runs Then the upload is rejected with an actionable error and no previous configuration is altered
Auto-Ordering and Aspect-Ratio-Preserving Scaling
Given logos are assigned to tiers When rendering the sponsor area Then logos are ordered by tier priority (Title, Supporting, Community) and within a tier by alphanumeric display name Given any logo is placed When it is scaled to fit available space Then its aspect ratio is preserved with ≤1% distortion and no dimension rounding exceeds 1 px Given kiosk width is 1080 px When rendering Then minimum logo heights are Title ≥ 56 px, Supporting ≥ 40 px, Community ≥ 28 px Given print output is an A6 table-tent at 300 DPI When rendering Then minimum logo heights are Title ≥ 10 mm, Supporting ≥ 7 mm, Community ≥ 5 mm Given available space cannot satisfy minimums for all tiers When layout is computed Then lower-priority tier logos collapse into a “+N more” indicator rather than violating minimum size or aspect ratio
Safe-Area, Alignment, and Minimum Size Enforcement
Given kiosk header/footer and print template placement zones are defined When logos are rendered Then a safe-area margin of at least 16 px (kiosk) or 5 mm (print) is maintained from all template edges And inter-logo gutters are at least 12 px (kiosk) or 4 mm (print) Given multiple rows are required When wrapping occurs Then rows maintain consistent vertical spacing (±1 px kiosk, ±0.5 mm print) and logos are vertically centered within their rows Given any layout When validation runs Then no logos overlap each other or fixed template elements and no logo is clipped
Contrast-Triggered Monochrome Variant Handling
Given a logo has a provided monochrome variant When its contrast against the current background fails the WCAG 2.1 non-text contrast threshold of 3:1 Then the system substitutes the provided monochrome variant for that logo only and records the before/after contrast ratio Given a logo lacks a monochrome variant When its contrast is < 3:1 Then the system applies a non-destructive knockout or outline behind the original to achieve ≥ 3:1 without altering the logo’s colors Given contrast remediation is applied When previews are refreshed Then the change is visible in both kiosk and print previews within 1 second
Brand Do-Not Rules Enforcement
Given any logo asset (SVG or PNG) When processed for rendering Then the system does not recolor, skew, rotate, add shadows/effects, or crop beyond uniform scaling and safe background treatments Given an SVG with embedded brand colors When exported to output Then original fill and stroke values are preserved exactly; CSS filters and color transforms are not applied Given an organizer attempts to globally tint logos or set a non-uniform scale When the change is submitted Then the action is blocked with an explanation referencing brand do-not rules and the existing rendering remains unchanged
Cross-Template Consistency (Kiosk Header/Footer and Print Table-Tent)
Given a saved sponsor logo configuration When generating kiosk header/footer and the printable table-tent Then the same set of logos, tier groupings, order, and relative size ratios (±2%) are applied in both outputs Given previews are opened side-by-side When a visual diff is run Then zero logos are missing or out of order and tier labels match exactly Given the kiosk size changes (small to large) When recomputing layout Then placement, padding, and ordering rules are reapplied automatically within 500 ms without user intervention
Offline Rendering and Cache Behavior
Given logos were previously uploaded on the device When the device is offline Then kiosk theming and print-ready assets render from cache with no missing images or placeholders Given the organizer edits tiers while offline When connectivity is restored Then changes sync within 5 minutes without duplication or loss and the latest server state matches the local preview Given cached assets exceed storage limits When eviction occurs Then the most recent configuration and the minimum assets required for current branding remain available for offline rendering
Rotating QR Snapshot & Expiry Controls
"As an event captain, I want a printable snapshot of our rotating QRs so that the table-tent remains scannable and accurate during the event."
Description

Captures a synchronized snapshot of active rotating QR targets (signup, donate, shifts) and composes a labeled, print-ready grid with shortlinks. Embeds timestamp and validity window, uses pre-provisioned static aliases to remain accurate as rotations continue, and allows optional expiry or auto-redirect updates on reconnect. Validates ECC level and module size for reliable scanning on common printers.

Acceptance Criteria
Offline One-Tap Snapshot of Rotating QRs
Given the device is offline and the event has active rotating QR targets for Sign Up, Donate, and Shifts When the user taps "Snapshot" Then the app creates a synchronized snapshot referencing pre-provisioned static aliases for all three targets And stores the snapshot locally with a unique version ID and checksum And completes within 2 seconds on a mid-tier device And shows confirmation "Snapshot saved" without requiring network access
Print-Ready Grid with Labels and Shortlinks
Given a saved snapshot When the user generates the print layout Then the app produces a single-page PDF (Letter and A5 selectable) at 300 DPI And displays three QR codes labeled "Sign Up", "Donate", "Shifts" And renders shortlinks below each label, all lowercase, <= 24 characters, clickable in the PDF And enforces quiet zone >= 4 modules and alignment grid And the PDF file size is <= 2 MB
Timestamp and Validity Window Display
Given the event's timezone is set When the print layout is generated Then the snapshot timestamp is shown as "YYYY-MM-DD HH:MM TZ" using the device's local time with offset And the validity window is displayed as "Valid until <date/time>" And the default validity window is 24 hours, configurable between 1 hour and 7 days And the displayed timestamp is within 2 seconds of the device clock Given the validity window has elapsed When the QR or shortlink is scanned Then the user is redirected to the configured fallback URL with HTTP 302 and a human-readable "This code has expired" message
Static Alias Continuity During Ongoing Rotations
Given live rotation continues server-side after snapshot When a printed snapshot QR or its shortlink is scanned within the validity window Then resolution redirects to the current canonical target for the corresponding flow (Sign Up, Donate, Shifts) And no 404/410 is returned And median redirect time is < 400 ms from a Tier-1 region And the alias mapping is logged with snapshot version ID
Optional Expiry and Auto-Redirect on Reconnect
Given "Auto-redirect on reconnect" is enabled And the device is offline at snapshot time When the device reconnects Then the app updates the static alias targets to the latest live endpoints within 10 minutes and records an "updated at" timestamp Given an explicit expiry time was set When the expiry time passes Then subsequent scans redirect to the configured fallback URL And if "Auto-redirect on reconnect" is disabled, no alias targets are changed on reconnect
ECC Level and Module Size Validation for Reliability
Given QR composition rules are applied When the PDF is generated Then each QR uses error correction level >= M and a minimum printed module size of 0.5 mm (>= 6 px at 300 DPI) And a quiet zone of >= 4 modules surrounds each code And the validator flags and blocks export if any rule is violated, presenting remediation hints And a test print on a 600 DPI laser printer is scannable by iPhone and Android default camera apps at 0.5 m in <= 2 seconds in 200–500 lx ambient light (10/10 scans per device)
Print-Ready Table-Tent Export
"As a booth volunteer, I want a print-ready table-tent file from my phone so that I can quickly print or send it to a copier without design tools."
Description

Generates a print-ready PDF for standard table-tent sizes (e.g., half-letter, A5) with bleeds, crop marks, safe margins, and optional back-side instructions. Produces CMYK or sRGB output as configured, rasterizes when necessary while preserving vector QRs and typography, and optimizes file size for mobile sharing. Supports one-tap AirPrint and system share, and functions offline using cached assets and theme settings.

Acceptance Criteria
Standard Sizes with Bleeds, Crop Marks, and Safe Margins
Given QuickBrand theme is applied and a table-tent export is initiated When the user selects "Half-Letter (5.5 x 8.5 in)" or "A5 (148 x 210 mm)" Then the generated PDF trim size matches the selected standard size And the PDF includes 0.125 in (3 mm) bleed on all sides And crop marks are present at each corner with 0.125 in (3 mm) offset and 0.25 in (6 mm) length And all content (text, logos, QR) remains inside a 0.25 in (6 mm) safe margin from the trim And a print preflight reports no page size, bleed, or mark warnings
Configurable Color Output (CMYK or sRGB)
Given a color mode setting of "CMYK" or "sRGB" When exporting the table-tent PDF Then the PDF embeds an OutputIntent ICC profile corresponding to the selected mode And all rasterized assets are converted to the selected color space And black body text remains 0/0/0/100 in CMYK or #000000 in sRGB And preflight shows no untagged or mixed color space objects outside the selected mode
Vector Preservation with Conditional Rasterization
Given the layout includes QR codes, typography, and sponsor logos with effects When exporting the PDF Then QR codes are embedded as vectors (not images) and resolve as vector objects in preflight And all typography remains vector with subset-embedded fonts (no rasterized text) And only elements requiring flattening are rasterized at ≥300 ppi And no QR codes or text objects are rasterized in the output
Mobile-Share Optimized File Size
Given the default export quality settings When exporting a single-sided or double-sided table-tent at Half-Letter or A5 Then the PDF file size is ≤3 MB for single-sided and ≤5 MB for double-sided And embedded raster images are not downsampled below 200 ppi at final size And fonts are subset and unused glyphs removed And the PDF is linearized (fast web view enabled)
Offline Export Using Cached Theme and Assets
Given the device is offline and the QuickBrand theme, sponsor logos, and QR snapshot are cached locally When the user exports the table-tent Then the PDF is generated successfully using cached event name, colors, logos, and QR snapshot And no network requests are attempted during export And if any required asset is not cached, the user is prompted to reconnect and the Export action is disabled until caching completes
One-Tap AirPrint and System Share
Given a generated PDF is ready When the user taps Print Then the native print dialog (e.g., AirPrint) opens with the PDF preselected within 3 seconds and defaults to 100% scale (no auto-scaling) And when the user taps Share Then the system share sheet opens with the PDF attached and common targets available (e.g., Files, Mail, Messages)
Optional Back-Side Instructions and Duplex Alignment
Given the "Include back-side instructions" setting is toggled on or off When exporting the table-tent Then the PDF contains two pages when on and one page when off And both pages include bleeds and crop marks And back-side orientation is set for short-edge duplex printing, aligning front/back panels within ±1 mm at trim
Brand Compliance Enforcement & Proof Pack
"As a compliance officer, I want a proof pack and enforced brand rules so that we can document approvals and avoid brand violations."
Description

Validates theme settings against coalition brand guidelines, including approved color pairs, required sponsor order, minimum logo sizes, and disclaimer text. Blocks publish on hard violations and offers guided fixes. Exports a proof pack (PDF plus JSON) with thumbnails, color values, contrast report, placement grid, timestamps, and approver notes for audit and record-keeping.

Acceptance Criteria
Color Pair and Contrast Enforcement
Given a user selects brand colors in QuickBrand When the theme is applied Then the system validates the selection against the coalition’s approved color pair list and rejects any unapproved pair And the system computes contrast ratios per WCAG 2.2 And normal text must be >= 4.5:1 and large text/icons must be >= 3:1 And any failure is flagged as a hard violation with the failing elements identified and a list of compliant alternative pairs suggested
Sponsor Logo Order and Minimum Size Validation
Given the coalition sponsor set and order are defined in brand rules When logos are placed on kiosk and table‑tent outputs Then logos render in the exact required order as specified by the rules And each required sponsor logo is present And each rendered logo meets or exceeds the minimum size (width/height in px and corresponding mm at target DPI) And any missing logo, out‑of‑order placement, or sub‑minimum size triggers a hard violation with a guided fix (auto‑sort, auto‑scale, or add missing asset)
Required Disclaimer Text Presence and Locking
Given required disclaimer text and allowed placeholders are defined in brand rules When the theme is applied Then the disclaimer renders in the designated area using the exact required copy with only allowed placeholders substituted And editing outside allowed placeholders is disabled in the UI And missing, altered, or truncated disclaimer content triggers a hard violation with a one‑tap insert‑correct copy action
Publish Blocking and Guided Fixes Panel
Given one or more hard violations exist (unapproved color pair, contrast fail, missing/altered disclaimer, missing/out‑of‑order/sub‑minimum sponsor logo) When the user attempts to Publish the theme Then the Publish action is blocked And a violations panel lists each issue with rule ID, impacted artifact(s), and severity And each listed issue includes an actionable guided fix And once all hard violations are resolved, the Publish button becomes enabled within 1 second and an audit entry is recorded with timestamp, user, and resolved rule IDs
Proof Pack Export: Contents, Format, and Integrity
Given all hard validations pass and the user taps Export Proof Pack When the export completes Then two files are produced: a PDF and a JSON And both files use the naming pattern EventName_YYYYMMDD_HHMM_Local-ProofPack.(pdf|json) And the PDF includes: thumbnails of kiosk and table‑tent final renders, placement grid overlay with measurements, contrast report table, color values (HEX and RGB), and timestamps (created, approved) And the JSON includes: schemaVersion, ruleVersionIds, color palette values, contrast results per element, placement coordinates/sizes, sponsor list with order and rendered sizes, disclaimer text, timestamps, approver name and notes And a SHA‑256 hash of the JSON is embedded in the PDF metadata and matches the JSON file at export time
Offline Validation and Deferred Sync of Proof Pack
Given the device is offline When the user applies a theme and runs validation Then all validations execute using the most recently synced brand rules cache without requiring network And if the user exports the Proof Pack offline, the PDF and JSON are generated and saved locally and queued for upload And upon network restoration, the queued Proof Pack is uploaded to the event record within 60 seconds and marked as synced with server timestamp And if the rules cache is older than 30 days, a warning banner is shown but Publish remains allowed if no hard violations exist under the cached rules; the theme is re‑validated automatically after the next successful sync

Offline Languages

Pre-cached language packs (including RTL and large-type variants) let volunteers choose their language instantly—no signal needed. Paired with concise, icon-forward prompts, it shortens lines and improves accessibility for multilingual, SMS-first communities.

Requirements

Pre-cached Language Packs & Versioning
"As a field organizer with spotty service, I want all needed languages preloaded so that volunteers can choose their language instantly without waiting on a network."
Description

Bundle and cache language packs on-device to enable full UI localization without connectivity. Each pack includes UI strings, SMS templates, icon labels, RTL variants, font subsets, and metadata (locale code, version, checksum). Implement signed, delta-capable updates that apply opportunistically when online and roll back on failure. Provide a fallback chain (selected → site default → English) and storage management to evict least-used packs within a defined size budget. Admins can publish new languages remotely via config without app store updates.

Acceptance Criteria
Offline Language Selection Without Connectivity
Given the device has pre-cached packs for English (en) and Spanish (es) and is offline When the user selects Español from the Language menu Then all UI strings, icon labels, and prompts render in Spanish within 1 second And no network requests are attempted And SMS template previews display the Spanish variants And the selection persists after an app restart while still offline
Pack Composition and Integrity Verification
Given a language pack file for locale ar-EG with metadata {localeCode, version, checksum} When the pack is installed Then it includes UI strings, SMS templates, icon labels, RTL flags/variants, and font subsets for the locale And the checksum matches the downloaded bytes And the pack’s signature validates against the trusted public key And the pack is rejected and not activated if any required component is missing or integrity checks fail
Signed Delta Update with Atomic Rollback
Given es-MX v1.2 is installed and es-MX v1.3 is available as a delta When the device is online and idle Then the delta is downloaded and applied to a staging copy And the staged pack’s checksum and signature validate And the app atomically activates v1.3 only after validation succeeds And if any step fails, the app reverts to v1.2 automatically with no partial localization And the installed version and metadata reflect v1.3 only upon success And the update runs without blocking user interactions And the update attempt is logged with success/failure and reason
Localized Content Fallback Chain
Given the user’s selected locale is pt-PT and the site default is fr-FR When a UI string, SMS template, or icon label is missing in pt-PT Then the app resolves it from fr-FR And if still missing, resolves it from English (en) And the final rendered UI shows no missing keys or placeholder text And a diagnostic event records each fallback resolution with the source locale
Storage Budget and Least-Used Eviction
Given a cache size budget B is configured and the device already stores multiple language packs When downloading or installing another pack would exceed B Then the app evicts the least-used packs by usage score until total size is <= B And it never evicts the currently selected locale, the site default, or English (en) And eviction occurs before the new download is finalized so the budget is not exceeded at any time And the eviction and resulting total size are logged
Admin Remote Publish Without App Update
Given an admin publishes a new locale (bn-BD) via remote configuration When the device next performs a config sync or the app restarts while online Then the new locale appears in the Language list And its pack begins downloading in the background And the locale is not selectable until the pack download, checksum, and signature validation complete And no app store update is required for availability
RTL and Large-Type Rendering Conformance
Given an RTL locale (ar) is selected When navigating core screens (home, signup, donation, shift assignment) Then layout and navigation are mirrored, text aligns right, and direction-sensitive icons are flipped appropriately And SMS compose previews display RTL-aligned templates And no clipped or overlapping text occurs And when the Large Type variant of any locale is selected, text scales to the configured size with proper wrapping and tappable hit targets, using the locale’s font subsets
Instant Offline Language Switcher
"As a check-in volunteer, I want to switch languages in one tap so that I can help the next person quickly without restarting or hunting through settings."
Description

Provide a single-tap language selector that works entirely offline and applies immediately across signup, donations, and shift flows without app restart. Expose the switcher on the welcome screen and as a persistent control in kiosk mode. Persist the user’s choice per session and allow per-contact override during record creation. Ensure sub-200ms language swap, clear visual confirmation, and full accessibility (screen reader labels, logical focus order).

Acceptance Criteria
Welcome Screen One‑Tap Offline Switch
Given the device has no network connectivity and the app is on the Welcome screen, When the user taps the Language switcher and selects Language X, Then the UI text on the current screen updates to Language X within 200ms without any network request and without restarting the app. Given the language is changed, When the selection is applied, Then a visual confirmation displaying "Language: X" appears for 1–2 seconds and dismisses automatically.
Persistent Kiosk Mode Language Control
Given Kiosk Mode is enabled, When any screen in signup, donation, or shift flows is displayed, Then a persistent language control is visible, reachable, and tappable without overlapping primary actions on devices ≥360dp width. Given Kiosk Mode is enabled, When the language control is activated, Then the language list opens offline, the current selection is indicated, and closing the list returns focus to the previously focused element.
Sub‑200ms Global Language Swap Across Flows
Given a language is selected, When the user navigates between Signup, Donation, and Shift Assignment screens, Then all static labels, prompts, and validation messages reflect the selected language immediately without app restart. Given a language switch occurs, When measured on target devices, Then 95th‑percentile language swap time is <200ms across 100 swaps performed offline. Given the language changes mid‑form, When the user resumes input, Then existing field values remain intact and only localized text resources refresh.
Session‑Level Language Persistence
Given the user selects Language X during a session, When they navigate across screens or the app is backgrounded and resumed within the same session, Then Language X remains active. Given the app is terminated and relaunched (new session), When no language is explicitly selected, Then the app starts in the default language and does not retain Language X from the prior session.
Per‑Contact Language Override at Record Creation
Given the user is creating a new contact, When Preferred Language Y is set on the contact form and the record is saved, Then the contact record stores Y and the session UI language remains unchanged. Given Preferred Language Y is set for the contact, When receipt/reminder previews are shown in the creation flow, Then the previews render in Y while the rest of the UI remains in the session language.
Accessibility and Visual Confirmation of Language Change
Given a screen reader is enabled, When focus lands on the language control, Then it announces an accessible label and the currently selected language. Given a new language is selected, When the change is applied, Then an accessible live announcement communicates "Language set to X" and focus order remains logical with no focus loss. Given the language control and list are displayed, When inspected, Then all items meet WCAG 2.1 AA focus visibility and 4.5:1 contrast and are reachable via hardware keyboard/Tab where applicable.
RTL and Large‑Type Variants Apply Offline
Given the device is offline and the user selects an RTL language, When the change is applied, Then layouts mirror appropriately, text aligns right, and icons follow locale mirroring rules with no clipped or overlapping text on the top 10 most‑used screens. Given the OS large‑type/text scaling is enabled, When a language is selected, Then text scales using the pre‑cached large‑type pack without horizontal scrolling in form fields or truncation of primary buttons.
RTL and Bidirectional Layout Support
"As an Arabic-speaking volunteer, I want the interface to read right-to-left so that it feels natural and I can enter information accurately."
Description

Enable automatic UI mirroring and correct text direction for RTL locales (e.g., Arabic, Hebrew, Urdu). Implement bidi-aware input fields so names, addresses, and free text render and cursor-navigate correctly, while numbers, phone/SMS fields, and amounts remain LTR. Localize punctuation, list ordering, and iconography orientation where appropriate. Include per-screen QA hooks to verify directionality and truncation, and ensure PDFs/receipts and SMS previews render correctly in RTL.

Acceptance Criteria
App-Wide RTL Mirroring on Locale Switch (Offline Supported)
Given the device is offline and an RTL language pack (e.g., Arabic) is pre-cached When the user selects the RTL language from the language picker Then the app re-renders with mirrored RTL layouts across all visible UI without any network calls within 3 seconds Given an RTL locale is active When navigating Home, Signups, Donations, Shifts, Impact Board, and Settings Then all containers, alignments, and scroll origins reflect RTL with no LTR remnants Given the app is relaunched When the prior locale was RTL Then RTL mirroring persists across sessions Given the system locale differs from the app locale When the app locale is RTL Then the app enforces RTL mirroring regardless of system locale
Bidirectional Text Entry Behavior in Free-Text Fields
Given an RTL locale is active and an Arabic/Hebrew keyboard is used When typing mixed RTL and LTR text in Name, Address, and Notes fields Then characters shape/join correctly and line direction follows the Unicode Bidirectional Algorithm Given the caret is moved via tap or arrow keys When navigating within mixed-direction text Then caret moves visually by glyph in RTL order; Backspace deletes the previous glyph; Delete removes the next glyph Given LTR numerals are inserted inside RTL sentences When editing the text Then numerals remain contiguous LTR runs without disrupting RTL word order Given input exceeds the field width When horizontally scrolling the field Then the scroll origin is right-aligned and the caret remains visible during entry Given placeholder text in RTL locale When the field is empty Then the placeholder aligns right and clears correctly on first input
LTR Behavior for Numeric/Phone/Currency Inputs in RTL Locales
Given an RTL locale is active When focusing Phone, SMS, Postal Code, and Amount fields Then text direction, alignment, and caret movement are LTR Given the user types Eastern Arabic or Latin digits When parsing and validating input Then values are accepted and normalized per spec while visual direction remains LTR Given a negative currency amount is entered When rendered in the field and summaries Then the minus sign appears to the left of the first digit and does not flip in RTL Given auto-formatting applies (grouping, hyphens, spaces) When the user types or pastes digits Then formatting follows regional rules and the field stays LTR Given mixed RTL text is pasted into a numeric field When the paste occurs Then non-numeric characters are rejected and the field remains LTR
Mirrored Iconography and Navigation Controls in RTL
Given an RTL locale is active When rendering navigation bars, lists, and drawers Then directional icons (chevrons, arrows, breadcrumbs) are horizontally mirrored while non-directional icons are unchanged Given the Back action is shown When the screen is displayed Then the back icon appears on the right and points right; forward/next chevrons appear on the left and point left Given swipe gestures reveal actions or drawers When swiping Then reveal directions are reversed appropriately for RTL and match visual affordances Given illustrations or logos are loaded When directionality would change semantics Then RTL-specific assets are used; otherwise assets are not mirrored Given sliders and progress bars are displayed When value increases Then the visual increase direction corresponds to RTL per design specification
Localized Punctuation and List Ordering in RTL
Given an RTL locale is active When rendering bulleted and numbered lists Then markers align on the right; item order is top-to-bottom, right-to-left; ordered list numerals render as an LTR run Given punctuation appears in RTL text When rendering commas, colons, quotes, parentheses Then localized glyphs and spacing are used (e.g., Arabic comma), and paired punctuation is correctly reversed Given neutral characters occur in mixed-direction text When laying out the line Then neutrals adopt surrounding directionality to avoid mis-ordering Given labels and validation messages include trailing punctuation When displayed in RTL Then punctuation appears at the visual left end according to RTL typographic norms
Locale-Appropriate Truncation and QA Hooks per Screen in RTL
Given an RTL locale is active When a string exceeds its container Then truncation occurs at the leading edge with an ellipsis on the left (or per design rule), and full text is available via tooltip or expand Given QA mode is toggled at Settings > Developer > RTL QA When enabled Then an overlay shows each component's resolved direction (LTR/RTL), truncation point, and overflow count Given QA mode is enabled When tapping a text component Then a toast logs component id, locale, direction, and shaping engine used Given long dynamic RTL data (e.g., street names) When wrapped across lines Then wrapping starts from the right edge and preserves word integrity without clipping
RTL Rendering in PDFs, Receipts, and SMS Previews
Given an RTL locale is active When generating a PDF receipt Then paragraph direction is RTL; headings and body are right-aligned; numeric amounts remain LTR and align by decimal Given Arabic/Hebrew text is present in documents When exporting PDFs Then appropriate fonts with glyph shaping/ligatures are embedded; no missing glyphs appear Given an SMS preview is shown When composing receipt/reminder text Then message lines render RTL with numerals and URLs as LTR runs; punctuation and line breaks appear correctly Given PDFs are shared to external viewers When opened on devices without app fonts Then documents render correctly due to embedded fonts Given messages include links or QR codes When copied to the native SMS app Then visual order and clickability are preserved
Large-Type and High-Contrast Localizations
"As a low-vision supporter, I want bigger text and clear icons in my language so that I can complete forms independently at events."
Description

Ship large-type variants for each language pack that respect system dynamic type settings and offer in-app quick toggles (150% and 200%). Ensure layouts reflow without clipping, minimum 44px touch targets, and sufficient color contrast in all localized themes. Provide icon-forward prompts with concise text to reduce reading load, and include scalable vector assets and fonts to keep packs lightweight. Validate with screen readers and magnifiers across LTR and RTL.

Acceptance Criteria
Offline Large-Type Toggles Respect System Dynamic Type
Given the device is offline and a pre-cached language pack is active When the user taps the in-app Large Type 150% toggle Then all visible text scales to 150% within 300ms without app restart, flicker, or layout jump Given the device is offline and a pre-cached language pack is active When the user taps the in-app Large Type 200% toggle Then all visible text scales to 200% within 300ms and all controls remain operable Given the system dynamic type category is set (e.g., Large, Extra Large, Accessibility sizes) When the app launches in any language (LTR or RTL) Then base text sizes match the system mapping (±2% tolerance) before any in-app scaling Given any language (LTR or RTL) When the user toggles back to Default Then text size returns to the system dynamic type without residual scaling Given the app is relaunched offline When the same language pack loads Then the last selected Large Type setting (Default/150%/200%) is restored per language Given offline operation When toggling any Large Type setting Then no network requests are issued (verified by network logger = 0 calls)
Minimum 44px Touch Targets at Large Type
Given Large Type 150% or 200% is active Then every tappable control has a minimum 44x44 logical pixels hit area, including list rows, toggles, chips, and nav controls Given Large Type 150% or 200% is active Then adjacent interactive elements maintain at least 8px spacing to avoid target overlap Given automated accessibility audit runs on top 20 interactive screens Then target size violations reported = 0
Contrast Compliance in Localized Themes (AA)
Given any localized theme and language (LTR or RTL) at 150% and 200% Then text contrast >= 4.5:1 for normal text and >= 3:1 for large text per WCAG 2.2 AA Given any localized theme and language (LTR or RTL) Then interactive component boundaries/icons meet >= 3:1 contrast against adjacent colors Given disabled states are shown Then disabled text/icons maintain >= 3:1 contrast against their immediate background Given UI snapshots of key screens are audited Then contrast check failures = 0 and no text is embedded in raster images
Layout Reflow Without Clipping or Overlap (LTR/RTL)
Given Large Type 200% and a long-string locale (e.g., German) or RTL (e.g., Arabic) When navigating the top 25 user flows Then clipped, overlapped, or truncated text occurrences = 0; content reflows vertically; no horizontal scrolling is introduced except within intentional carousels Given dynamic button labels expand at 200% Then labels wrap to a maximum of 2 lines, remain fully readable, and buttons remain fully tappable Given form screens at 200% Then labels maintain association with inputs; placeholders do not occlude entered text; error messages are visible without overlap
Icon-Forward Prompts With Concise Text
Given any language at 150% or 200% Then each primary prompt includes an icon plus no more than 2 lines of text with a maximum of 45 characters per line (English-equivalent length); localized variants respect the 2-line limit Given secondary explanatory copy exists Then it is collapsed behind an info icon or expandable area by default Given a usability recognition test with n=10 participants per language When shown primary prompts without text Then >= 80% correctly identify the intended action from the icon alone
Accessible With Screen Readers and Magnifiers (LTR/RTL)
Given TalkBack (Android) or VoiceOver (iOS) is enabled at 150% and 200% Then focus order matches visual order in both LTR and RTL; all controls have localized labels, roles, and concise hints; duplicate announcements = 0 Given dynamic lists, toasts, and validation messages appear Then they are announced within 1 second and are reachable via accessibility focus navigation Given system magnifier/Zoom is used at 150% or 200% Then no content becomes unreachable; modals and bottom sheets are fully scrollable and focus is appropriately trapped Given Switch Control or keyboard navigation is used Then language and large-type toggles are fully operable without touch
Lightweight Packs: Vector Assets and Fonts Size Budget
Given a language pack with large-type variants is built Then total on-disk size per language (fonts + vectors + metadata) <= 2.5 MB and large-type support adds <= 10% overhead vs the base pack Given any language is switched while offline Then the switch completes with median latency <= 500ms and p95 <= 900ms on the reference device; no network calls are made Given assets are inspected Then UI uses scalable vector assets for icons/illustrations; any required raster images are shared across languages and each <= 100KB Given font packaging for each locale Then fonts are variable or subsetted to the locale; unused glyph ranges are stripped; text renders crisply at 200% without pixelation
Localized SMS Templates with Offline Queue
"As an event lead, I want receipts and reminders to go out in each person’s language even if we’re offline so that communication is clear and timely."
Description

Localize all automated SMS (receipts, reminders, confirmations) per language pack and allow composing and scheduling while offline. Queue messages with personalization tokens resolved locally; send automatically when connectivity returns. Enforce GSM-7/Unicode segmentation rules, include localized opt-out text, and fall back to a default language if a template is missing. Provide preview in the selected language and an audit trail synced to the Impact Board once delivered.

Acceptance Criteria
Compose and Schedule SMS Offline
Given the device has no network connectivity, When the user composes a receipt, reminder, or confirmation SMS and sets a send time, Then the message can be saved locally and placed into an outbox with its scheduled timestamp. And the compose screen remains fully usable offline and makes zero network requests. And queued items persist across app restarts and device reboots. And a user can edit or cancel a queued message before it is sent; edits update the queued record.
Language Pack Selection, Preview (RTL/Large-Type), and Fallback
Given pre-cached language packs exist (including RTL and large-type), When a user selects a language for the message, Then the localized template loads from local storage with no network calls. And the preview renders in the selected language; RTL languages are right-aligned with correct character order; large-type increases font size by at least 30% without truncation. And if the selected language lacks the specific template key, Then the system falls back to the default language for that template only and displays a non-blocking fallback indicator in the preview.
Personalization Token Resolution Offline
Given a localized template containing personalization tokens, When composing offline, Then all tokens resolve locally using cached recipient, event, and campaign data. And unresolved required tokens are flagged inline; scheduling is blocked until resolved or a defined default value is applied. And resolved values respect the selected language’s locale formatting for dates, times, and currency. And the preview shows the fully resolved message per recipient before queuing.
GSM-7/Unicode Detection and Segmentation
Given a resolved message body and opt-out text, When calculating message length, Then the system detects encoding (GSM-7 vs Unicode) and segments using standard lengths for single and concatenated messages. And the UI displays encoding type, characters remaining in the current segment, and total segment count prior to scheduling. And the queued payload records the final segment count to be sent to the gateway. And messages exceeding an organization-defined maximum segments require explicit confirmation before scheduling.
Localized Opt-Out Text Inclusion
Given any automated SMS is queued, When preparing the outbound payload, Then the localized opt-out text for the selected language and locale is appended according to policy. And the opt-out copy uses approved wording and is included in segmentation calculations. And if including the opt-out text would exceed the allowed segment limit, Then the user is prompted to shorten the main message or approve the additional segment(s) before scheduling.
Auto-Send, Ordering, and De-duplication on Reconnect
Given queued messages exist while offline, When network connectivity is restored, Then messages dispatch automatically without user action. And messages send in chronological order by scheduled timestamp; ties are ordered by creation time. And each message is sent exactly once using an idempotency key; transient failures retry with exponential backoff up to a configurable limit. And messages targeting recipients who opted out while offline are not sent and are marked Failed with reason “Opted out.”
Impact Board Audit Trail Sync with Delivery Outcomes
Given a message is sent or skipped, When the server returns an acknowledgment or delivery status, Then an audit entry is created containing message ID, recipient, language used, template key, token snapshot, segment count, timestamps, and outcome. And audit entries created offline are queued locally and synced to the Impact Board on connectivity. And the Impact Board shows per-message status (Queued, Sent, Delivered, Failed) and aggregates by template and language.
Language Admin Controls and Usage Analytics
"As a program manager, I want to enable the right languages for each site and see which are used so that I can staff and translate effectively."
Description

Offer remote-configurable language availability per campaign/site, default language per device or QR code, and the ability to hide experimental locales. Capture privacy-preserving metrics (language selection, switches per session, template fallback rates) and sync when online to inform capacity planning and the Impact Board. No PII is stored in metrics; provide export and rollback controls for newly published packs.

Acceptance Criteria
Remote Language Availability by Campaign and Site
Given an admin sets the allowed languages for Campaign A at Site X When a device assigned to Site X completes a config sync Then the language selector shows only the allowed languages for Site X Given the device is offline When the app launches after a successful config sync Then the last-synced allowed language list is enforced Given an admin removes a previously allowed language from Site X When the device next syncs Then that language no longer appears in the selector And any active session using that language is prompted to choose from the allowed list
Default Language via QR Code and Per-Device Settings
Given a QR code includes defaultLang=es and "es" is allowed at Site X When a volunteer starts a session by scanning the QR code Then the UI initializes in Spanish without showing the language picker Given a device default language is set to ar And no defaultLang is present in the QR code And "ar" is allowed at Site X When the app launches Then the UI initializes in Arabic (RTL applied) Given neither the QR code default nor the device default is allowed When the app launches Then the language picker opens filtered to the allowed list And a "default_unavailable" metric is recorded Given a QR code default is provided but the language pack is not cached and the device is offline When the app launches Then the language picker opens And a "pack_not_available_offline" metric is recorded
Hide Experimental Locales from Production
Given an admin marks locale "bn-BD" as Hidden When end users open the language selector Then "bn-BD" is not displayed or selectable Given a tester device is whitelisted for "bn-BD" When the tester opens the selector Then "bn-BD" appears with an "Experimental" badge Given a session is currently using "bn-BD" When an admin hides "bn-BD" and the device next syncs Then the session must select a permitted language before proceeding And a "experimental_hidden_migration" metric is recorded
Privacy-Preserving Language Usage Metrics
Given a user selects a language at session start When the session ends Then one metric event is stored containing only campaignId, siteId, language_code, selection_source, and a non-identifying session token And no PII (name, phone, email, address, free-text) is stored Given a user switches languages within a session When N switches occur Then a switch_count of N is recorded for that session And aggregate counts per language are updated Given a template string is missing in the active pack When a fallback to the base template occurs Then a template_fallback event is recorded with language_code and key And no user-entered content is stored
Online Sync and Offline Buffering of Metrics
Given a device has buffered metrics while offline When connectivity is restored Then the device uploads pending metrics within 2 minutes And successfully uploaded events are removed from the buffer Given the metrics buffer reaches its maximum capacity When new metric events are created Then the oldest events are dropped And a metrics_dropped_count summary is uploaded on next sync Given duplicate uploads are attempted due to retry When the server receives events with the same event_id Then duplicates are ignored and not double-counted
Metrics Export for Capacity Planning and Impact Board
Given an admin selects a date range, campaigns, sites, and languages When Export CSV is requested Then a CSV is generated with columns [date, campaignId, siteId, language_code, sessions, switch_count, template_fallbacks] And the export excludes PII And a secure download link is provided Given the Impact Board requests language usage data When metrics have been synced Then site-level percentages by language reflect the latest metrics within 15 minutes
Language Pack Versioning and Rollback
Given a new language pack version 2.3 is published for locale "es" When an admin triggers Rollback to version 2.2 in the Admin Console Then version 2.2 becomes the active version for all campaigns/sites using "es" Given devices have pre-cached version 2.3 When they next sync after rollback Then version 2.2 is marked active and 2.3 is deprecated And devices revert to 2.2 assets without requiring a full app update Given a rollback occurred When viewing the audit log Then an entry shows timestamp, actor, locale, from_version, to_version, and reason

SyncAssign

On reconnect, auto-syncs all captures, validates consent, and attaches each person to the right campaign, list, or shift. Sends receipts and confirmations with calendar links, carries over brought-by credit, and posts progress to the Impact Board—all without manual cleanup.

Requirements

Offline Auto-Sync with Conflict Resolution
"As a field organizer working offline, I want my captured data to auto-sync correctly when I reconnect so that I don’t spend time reconciling or losing entries."
Description

On network reconnect, automatically synchronize all locally captured signups, donations, and shift assignments from mobile to the server using batched uploads, retries with exponential backoff, and idempotent operations. Implement deterministic conflict resolution (e.g., last-write-wins with server authority and field-level merges where safe) and deduplication keyed by stable client-side IDs. Support partial success handling, resume-on-failure, and transactional grouping to preserve data integrity. Ensure low-memory/mobile-friendly operation, background execution, and telemetry for sync duration, failure rates, and throughput.

Acceptance Criteria
Auto Sync on Reconnect with Batching and Backoff
Given the device has 1–500 unsynced items (signups, donations, shift assignments) and connectivity changes from offline to online When the OS reports network availability Then the client initiates sync within 5 seconds And items are uploaded in batches of up to 50 items per request And transient errors (HTTP 5xx, timeouts) trigger retries with exponential backoff: initial 1s, factor 2.0, max 60s, up to 5 attempts per batch And successfully uploaded items are marked synced locally and are not re-uploaded in subsequent runs And sync stops after all items are successfully committed or terminal errors are recorded per item
Idempotent Uploads with Stable Client IDs
Given each item has a stable client-generated UUIDv4 and the client includes an Idempotency-Key per batch When the same batch or item is retried due to a transient error or reconnect Then the server performs idempotent upserts and does not create duplicate records And the server returns the canonical server entity ID and version for each item And the client verifies the mapping and confirms that no item is written more than once
Deterministic Conflict Resolution (LWW + Field-Level Merge)
Given an entity was modified offline and also changed on the server while offline When the client syncs the entity with last_known_version and updated_at timestamps Then last-write-wins is applied using server time for conflict ordering And fields designated merge-safe are merged as follows: set fields (e.g., tags, list memberships) use union; boolean consent uses revoked-overrides-granted; free-text notes append with timestamp and source And the resolved entity is consistent across repeated syncs And a conflict resolution log entry is created including entity ID, fields affected, rule applied, and timestamps
Deduplication Using Stable Client IDs
Given multiple client records reference the same stable client ID due to retries or app restarts When syncing to the server Then only one server record is created per stable client ID And the client updates local pointers to the returned server ID for all references And exactly one receipt/confirmation is triggered per unique transaction or signup
Partial Success and Resume-on-Failure
Given a batch contains items where some succeed and others fail with recoverable errors When the server returns per-item statuses Then the client marks successes as synced and persists failure reasons locally within 500 ms And only failed items are retried on subsequent attempts And the sync session summary reports counts for succeeded, failed, and retried items And the retry state survives app restarts and OS process kills
Transactional Grouping Preserves Integrity
Given a donation, its receipt, and a contact update are grouped as one transaction with a transaction_id When the group is synced Then the server commits all writes atomically or rolls back all on failure And the client retries the entire group atomically using the same transaction_id for idempotency And the server returns a group-level status and correlation ID that the client stores
Background Sync, Low-Memory Operation, and Telemetry
Given the app is backgrounded and the device has limited memory or battery When a background sync opportunity occurs or the app resumes Then the sync runs within OS constraints, limiting peak memory to 50 MB and yielding on memory pressure without data loss And the client respects metered network settings and pauses on low power mode unless user overrode defaults And telemetry is emitted per run: sync_duration_ms, items_synced_count, throughput_items_per_min, retry_count, failure_rate_pct, memory_peak_mb And the 95th percentile sync_duration_ms for 100 items over 4G is  30, measured in pilot telemetry
Consent Validation & Audit Trail
"As a compliance-conscious admin, I want consent automatically validated and logged so that outreach stays legal and trustworthy."
Description

Validate and record outreach consent at sync time before any messaging or list assignment. Enforce jurisdiction-specific rules (e.g., opt-in type, age gates) and block downstream actions if consent is missing or invalid. Persist a detailed audit trail including consent source, timestamp, method (checkbox, verbal, form), terms/policy version, collector identity, and geo context. Provide immutable logs, exportable reports, and mechanisms to revoke or update consent, with safeguards to prevent messaging to non-consented contacts.

Acceptance Criteria
Consent Gate at Sync Before Any Downstream Action
Given a device reconnects with queued captures, when SyncAssign starts, then each record’s consent is validated before any list/shift assignment or any message/receipt is queued or sent. If a record’s consent is missing or invalid, then no downstream actions are created for that record, the record is flagged consent_required with machine-readable reasons, and the sync continues for other records. A per-sync summary reports total processed, blocked_count, and blocked_reasons distribution. Performance: validate ≥500 records with p95 latency ≤5s on a typical mobile reconnect.
Jurisdiction-Specific Rules and Age Gates Enforcement
Consent validation uses the org-configured policy map keyed by geo context (country/region) and channel (email, SMS, calls). If geo context is unknown, the most restrictive policy is applied. Age gates: if the person’s age is below the policy threshold for any channel, that channel is marked non-consented and blocked. Channel rules: SMS requires explicit, recorded opt-in; email follows the jurisdiction’s opt-in type (e.g., single vs. double) from policy; voice calls require prior express consent where configured. Outcome is per-channel: valid, invalid, or unknown with machine-readable reason codes.
Comprehensive Immutable Audit Trail
On each consent decision (create, update, revoke), append an audit record containing: person_id, channels[], consent_status per channel, source, UTC timestamp (ISO 8601), method (checkbox, verbal, form), terms_policy_version, collector_identity (user_id/device_id), geo_context (lat/long if available + country/region), evidence_reference (file_id/hash if applicable), decision_engine_version, record_checksum, and prior_audit_id (if updating). Audit storage is append-only: direct edits or deletes are blocked; corrective entries are new appends referencing prior_audit_id. Tamper checks: record_checksum verification succeeds for 100% of retrieved records; failed verifications are surfaced as integrity_alerts. Audit records are queryable by person_id and date range with p95 read latency ≤300ms for indexed queries.
Consent Revocation and Update Propagation
When consent is revoked from any channel (unsubscribe, STOP, staff action, API), within 60 seconds all queued and scheduled messages for the affected channels are canceled and future sends are blocked. An audit entry is appended with revoke reason, origin, and timestamp; prior consent entries remain intact. When consent is updated to valid with sufficient evidence, new sends/assignments are allowed only after the update audit entry exists and validation passes on next check. Revocation cascades across all campaigns/lists for the person, and UI shows channel status as non-consented within 60 seconds.
Exportable Consent Reports with Filters and Redaction
Provide CSV and JSON exports of consent audit data filterable by date range, campaign/list, jurisdiction, channel, consent_status, and collector_identity. Exports include required fields: person_id, channel, consent_status, source, timestamp, method, terms_policy_version, geo_context, evidence_reference, and reason codes. PII redaction toggle: when enabled, email/phone are hashed; when disabled, full values appear; default is redacted. Role-gated access: only users with Data Admin may create exports; all exports are logged with requester_id and purpose. Performance: generate up to 100k rows within 2 minutes and stream larger exports with pagination tokens.
Messaging Safeguards and Send-time Consent Checks
Every outbound send pipeline (email, SMS, call tasks, calendar invites) performs a send-time consent check using latest audit state; if non-consented or unknown, the send is blocked. Blocked sends create a non_delivery event with code send_blocked_consent and do not decrement daily send quotas. Send reports display attempted_count, sent_count, blocked_consent_count per channel; blocked_consent_count > 0 never results in any actual sends to those contacts. Integration tests confirm zero deliveries to non-consented contacts across batch, triggered, and retry scenarios.
Offline Capture Reconciliation and Duplicate Merge with Consent Precedence
On reconnect, duplicate persons are merged per org rules; consent state resolves to the most recent explicit consent with sufficient evidence; conflicts default to the stricter outcome (non-consent over consent). If evidence is missing or unverifiable for a claimed consent, that channel is treated as non-consented until updated. Each merge writes an audit entry recording merged source_ids, chosen consent state per channel, and rationale (most_recent or stricter_policy). No downstream actions are created for a merged record until post-merge consent validation completes successfully.
Smart Entity Association
"As a volunteer lead, I want new contacts automatically attached to the correct campaign, list, or shift so that follow-ups and staffing are accurate without manual cleanup."
Description

Automatically attach each person to the correct campaign, list, or shift based on capture context (entry form, QR/ref link, kiosk mode, event/shift roster, geo), preconfigured mapping rules, and existing CRM relationships. Resolve ambiguities with priority rules and fallbacks, surface exceptions for review, and ensure associations are idempotent. Perform person deduplication using configurable match keys (email, phone, name+zip) and merge policies to prevent duplicates while preserving history and relationships.

Acceptance Criteria
QR Signup to Specific Campaign, List, and Shift
Given a captured signup contains QR ref "CMPA_LST12_SFT34" mapped to Campaign A, List 12, and Shift 34 When SyncAssign processes the capture on reconnect Then the person is associated to Campaign A, added to List 12, and assigned to Shift 34 within 5 seconds of sync start And the association source is recorded as "qr_ref CMPA_LST12_SFT34" And no additional campaigns, lists, or shifts are added by this step
Conflicting Mapping Rules from Geo and Event Roster
Given the capture includes GPS within Region R mapped to Campaign B and appears on an Event Roster for Campaign C And the priority rules are [event_roster > ref_link > geo > default] When SyncAssign computes associations Then the person is associated to Campaign C per priority And no association to Campaign B is created And an audit entry records inputs, applied priority rule, and final decision
Repeat Sync After Network Flap
Given the same capture payload is processed twice within 24 hours using the same idempotency key When the second processing attempt runs Then zero new person, membership, or shift-assignment records are created And all previously created associations remain unchanged And the job result is marked "idempotent_skip" with a reference to the original job ID
Dedup Using Email, Phone, and Name+ZIP
Given two captures: A with email e@example.com and B with phone +15551234567 and both with name "Pat Lee" and ZIP 94110 And match keys are configured as [email OR phone OR (name+zip)] When SyncAssign processes both captures Then exactly one person record exists after processing And both captures are linked to that person And the dedup decision and matched keys are written to the audit log
Merge Preserves History and Relationships
Given two existing person records P1 and P2 match per merge policy with primary = most_recent_activity And P1 has 3 donations, 2 shift assignments, and is on lists L1 and L2; P2 has 1 donation, 1 assignment, and is on lists L2 and L3 When SyncAssign merges P1 and P2 Then the resulting person has 4 donations, 3 shift assignments, and memberships L1, L2, and L3 with no duplicates And all relationships (including brought_by links and campaign affiliations) from both are preserved And the losing record’s external IDs are retained as aliases And an audit entry records field-level winners and merged relationships
Respect Existing Campaign Affiliation
Given a person is already affiliated with Campaign D with exclusive = true And a new capture maps to Campaign E When SyncAssign processes the capture Then the person remains affiliated to Campaign D And the Campaign E association is skipped due to exclusive affiliation And the skip reason is recorded in the audit log
Unresolvable Ambiguity Routed to Review Queue
Given a capture matches two existing persons by name+zip but neither email nor phone, and conflicting unique IDs exist When SyncAssign attempts association Then no auto-merge and no new person creation occur And an exception is created in the Review Queue with reason "ambiguous_person_match", the candidate record IDs, and a capture payload fingerprint And the capture is retried automatically within 15 minutes after a manual resolution is applied
Automated Receipts & Calendar Confirmations
"As a donor or volunteer, I want instant receipts and calendar confirmations so that I have proof and reminders without waiting."
Description

Upon successful sync, trigger personalized receipts for donations and confirmations for signups/shift assignments using localized templates and the recipient’s preferred channel (email/SMS). Include calendar invitations (ICS attachments and smart links for Google/Apple) with reminders, as well as deep links back to GiveCrew for updates or cancellations. Prevent duplicate sends via idempotent message keys, and track delivery, bounces, and link engagement for reporting. Respect consent and quiet hours, and queue messages if channels are temporarily unavailable.

Acceptance Criteria
Personalized Receipts and Confirmations on Successful Sync
Given a successful sync that created or updated donation and shift-assignment records with valid contact profiles When the sync completes Then exactly one donation receipt is generated per donation and one confirmation per shift assignment for each recipient And the template language matches the recipient’s locale And the message is sent via the recipient’s preferred channel (email or SMS) And the send is logged with message ID, recipient, channel, template ID, and timestamp
Calendar Invitations Included with Confirmations
Given a shift confirmation delivered via email When the email is composed Then it includes a single ICS attachment with a VEVENT matching the shift title, start/end time, timezone, and location And the ICS includes VALARM reminders that match the organization’s reminder settings Given a shift confirmation delivered via SMS When the SMS is composed Then it includes smart links for Google Calendar and Apple Calendar that prefill the same shift details as the ICS And opening either link creates an event with the correct title, time, timezone, and location
Deep Links Enable Update or Cancellation in GiveCrew
Given a receipt or confirmation is sent When the recipient taps the deep link in the message Then GiveCrew opens (app if installed, otherwise mobile web) to the person’s donation or shift-assignment detail And the user can update or cancel the shift or edit their signup as permitted by role And after a successful change, the record is updated server-side and the UI reflects the new status
Idempotent Messaging Prevents Duplicate Sends
Given idempotent message keys derived from recipient ID, object ID (donation or assignment), event type (receipt or confirmation), and template version When the same sync event is processed multiple times or a send is retried Then at most one message is delivered per unique key and channel And subsequent attempts return a no-op with the original message ID And analytics count only one send for the unique key
Consent and Quiet Hours Enforcement
Given a recipient without valid consent for a channel When a receipt or confirmation would be sent on that channel Then the message is not sent on that channel And the suppression reason is logged for reporting Given organization quiet hours are configured (e.g., 21:00–08:00 recipient local time) When a message becomes ready during quiet hours Then the message is scheduled for the next permissible window outside quiet hours And the scheduled time is stored and auditable
Channel Unavailability Queueing and Retry
Given the preferred channel’s provider returns a transient failure (e.g., rate limit, timeout, 5xx) When attempting to send a receipt or confirmation Then the message enters a retry queue with its idempotency key preserved And the system retries according to the retry policy until success or the policy’s terminal condition is met And upon provider recovery, the message is sent once without duplication And final status (delivered or failed) is recorded with the last error code and timestamp
Delivery, Bounce, and Engagement Tracking
Given a receipt or confirmation is sent When provider delivery webhooks are received Then the message record updates to Delivered, Bounced, Blocked, or Failed with provider status code and timestamp Given the message contains tracked links (deep links and calendar smart links) When a recipient clicks a link Then the system records a unique click event with message ID, link type, timestamp, and device context where available And aggregate reporting reflects sends, deliveries, bounces, and unique clicks per template and channel
Referral Credit Carryover
"As a canvasser, I want my referral credit to carry over after sync so that my contributions are recognized and reported accurately."
Description

Carry over and attribute “brought-by” referral credit from offline captures through sync to the appropriate referrer, updating leaderboards and attribution reports. Handle edge cases such as merged contacts, duplicate referrals, and reassigned shifts. Prevent gaming with per-event limits and idempotent attribution. Expose referral metadata on contact profiles and in analytics for recognition and incentive programs.

Acceptance Criteria
Offline Capture Carryover on Sync
Given an offline capture includes referrer_id R and captured person C for campaign K (and optional shift S) When the device reconnects and SyncAssign runs Then one and only one referral attribution record exists linking C->R scoped to K (and S if present) by the end of the sync job And the attribution’s source=offline and captured_at equals the original device capture timestamp And C.profile shows referrer=R after sync completes And R.metrics.referred_count increases by 1 for campaign K after the next metrics refresh cycle
Attribution Persistence After Contact Merge
Given referrer contacts R1 and R2 are merged into surviving contact R* When the merge completes Then all referral attributions targeting R1 or R2 now target R* with the same scope (campaign/event/shift) And duplicate attributions caused by the merge (same referee C, same scope) collapse to a single record And audit history retains original referrer_ids and merge operation id
Idempotent Attribution on Retries
Given the same capture event is retried N times due to reconnects with identical idempotency_key = device_id + local_capture_id + campaign_id (+ shift_id) When SyncAssign processes these retries Then only one referral attribution exists in storage And subsequent retries return a no-op result and do not increment counts or leaderboards And re-import via another sync path with the same idempotency_key also does not create additional credit
Shift Reassignment and Attendance Vesting
Given referred person C is assigned to shift S1 for event E and credited to referrer R When C is reassigned to shift S2 within the same event E Then the referral attribution remains credited to R and scoped to E When C is reassigned to a different event E2 before attendance Then the attribution follows C to E2 and counts under E2 And referral status remains pending until C is checked-in; upon check-in status becomes vested and counts toward leaderboards; if C cancels or no-shows, status becomes cancelled and does not count
Per-Event Referral Limits and Anti-Gaming
Given per_event_referral_limit L is configured for event E When referrer R accrues more than L vested referrals for E Then only the first L vested referrals count in leaderboards and reports; additional referrals are marked over_cap=true and excluded from counts And multiple referrals of the same person C by R within E (across any shifts) count as one toward limits and totals And self-referrals (R == C) are rejected and produce no attribution And when duplicate captures exist for (R, C, E), exactly one attribution persists after deduplication
Leaderboards and Reports Update on Sync
Given new vested referral attributions are created or updated during sync When the reporting job runs Then the referrer leaderboard and attribution reports reflect updated totals within the same reporting interval And leaderboards count referrals by vested_at date and event; backfilled attributions appear in the correct historical interval and event scope And removing or cancelling a referral decreases totals in the next reporting interval
Referral Metadata Visibility in Profiles and Analytics
Given a referral attribution exists between referrer R and referee C When viewing C’s profile Then referrer name, link to R, attribution_status, captured_at, vested_at (if any), campaign, event, and shift are visible When viewing R’s profile Then total referred (lifetime), referred in current campaign/event, last_referred_at, and a list of latest referees are visible And analytics export and API include fields: referrer_id, referee_id, campaign_id, event_id, shift_id, attribution_status, captured_at, vested_at, over_cap, source, idempotency_key
Impact Board Auto-Posting
"As a campaign director, I want progress to appear automatically on the Impact Board so that my team sees up-to-date wins without manual updates."
Description

After records are successfully synced and validated, post aggregated progress updates to the Impact Board in near real time, including metrics like new signups, donations (amount and count), and filled shift slots. Ensure correctness with duplicate protection, transactional updates, and the ability to roll back or recompute after merges. Support filtering by campaign, time window, and location, and queue updates for offline devices to display once connected.

Acceptance Criteria
Near-Real-Time Auto-Posting After Sync
Given a device reconnects and SyncAssign completes a validated sync batch for campaign C When the batch is acknowledged with status=validated Then the Impact Board updates aggregate metrics for C within 10 seconds of acknowledgment And the metrics include new_signups_count, donation_count, donation_amount_total (currency preserved), and filled_shift_slots_count And only records with validation_status=passed from the batch are included And the board displays a last_updated timestamp in the organization's timezone
Duplicate Protection and Idempotent Replays
Given one or more records are delivered multiple times due to retries or reconnects When events share the same idempotency_key or the same (entity_type, entity_id, version) Then aggregates are incremented at most once per unique record version And replaying the entire batch produces no net change in totals And conflicting duplicates (e.g., same external_id with differing payloads) are flagged and excluded from aggregates until resolved
Transactional Aggregate Update per Batch
Given a validated batch contains N>=1 records affecting multiple metrics When applying aggregate updates to the Impact Board Then updates are committed atomically per (campaign, time_window, location) key And on any failure, no partial totals are visible and the operation is retried up to 3 times with backoff And a correlation_id linking the batch to the aggregate version is persisted in the audit log And a background recomputation of the same time range yields the same totals
Rollback and Recompute After Merge/Unmerge
Given two person records are merged resulting in deduplication of K previously-counted items When the merge job completes Then affected aggregates for impacted campaigns, locations, and time windows are recomputed within 60 seconds And counts decrease by exactly K where applicable, and donation_amount_total adjusts accordingly And an audit entry records the adjustment with reason=merge and references to source record IDs And if the merge is reversed within 24 hours, previous aggregates are restored within 60 seconds
Filtered Views by Campaign, Time Window, and Location
Given a user selects campaign=C, time_window=Last 7 days, and location=L When the filter is applied Then metrics include only records matching C and L with timestamps within the last 7 calendar days in the organization's timezone And window boundaries are inclusive of start and exclusive of end And changing any filter updates the board within 500 ms using cached aggregates And if no matching data exist, zeros are displayed without errors
Offline Queue and Deferred Display
Given a device is offline for T minutes When server-side aggregates change during T Then the device shows the last-synced snapshot with an "as of" timestamp And upon reconnection, within 10 seconds the device applies queued delta updates, or triggers a full recompute if T>7 days, to display current aggregates And no duplicate increments are displayed during replay And the offline queue is capped at 10,000 events; overflow triggers a full recompute on reconnect
Sync Error Handling & Reconciliation
"As an operations manager, I want an easy way to detect and resolve sync errors so that data stays clean and the team keeps moving."
Description

Provide robust error handling with a visible retry queue, per-record diagnostics, and actionable error categories (consent failure, mapping ambiguity, network/server error). Enable safe re-submission, record-level fixes (e.g., update consent, adjust association), and bulk remediation tools for admins. Notify responsible users when intervention is needed, and capture observability signals (logs, metrics, traces) to monitor health and SLA adherence.

Acceptance Criteria
Visible Retry Queue on Reconnect
- Given the device reconnects after being offline with unsynced items, when the user opens SyncAssign or the app foregrounds, then a Retry Queue banner is shown with total pending count and a link to the queue. - Given the user opens the Retry Queue, when items exist, then each entry shows person identifier, record type (signup, donation, shift), last error category, last attempt timestamp, attempt count, and next scheduled retry time. - Given there are no pending items, when the user opens the Retry Queue, then the view shows "All caught up" and the banner is hidden.
Per-Record Diagnostics and Actionable Categories
- Given a failed record in the Retry Queue, when the user opens its details, then the error category is one of [consent_failure, mapping_ambiguity, network_error, server_error, validation_error]. - Given the user views details, then the UI displays the last 5 attempts including timestamp, HTTP status/socket error, duration, and correlation/trace ID. - Given the user views details, then the UI shows an actionable next step aligned to the category (e.g., collect consent, choose mapping, retry later) with an action button or deep link.
Safe Re-Submission & Idempotency
- Given a failed record is eligible to retry, when the user taps Retry or Auto-Retry triggers, then the sync request includes an idempotency key stable per source record and operation. - Given the server previously processed the operation, when a duplicate retry occurs, then no duplicate donation, signup, assignment, receipt, or confirmation is created and the client receives an idempotent success response. - Given a retry succeeds, when receipts/confirmations are sent, then only one receipt/confirmation is delivered to the contact and the Retry Queue entry is cleared. - Given repeated failures of the same category, when auto-retry runs, then exponential backoff with jitter is applied up to a maximum of 5 attempts before requiring intervention.
Consent Failure Remediation Flow
- Given a record failed with category consent_failure, when the responsible user opens remediation, then the UI blocks retry until required consent fields (e.g., SMS opt-in checkbox, timestamp, source) are completed or the record is marked do_not_contact. - Given the user captures consent in-app, when saved, then consent metadata (who/when/how) is stored and included on the next retry payload. - Given consent remains not obtained, when the user marks do_not_contact, then the item is removed from retry for messaging operations and a non-retryable status is logged. - Given remediation actions occur, when retried, then the record sync succeeds and audit logs record user, action, timestamp, and previous state.
Mapping Ambiguity Resolution
- Given a record failed with category mapping_ambiguity, when the admin opens the resolver, then the system presents up to 5 candidate campaigns/lists/shifts with relevance scores and key attributes (name, dates, location). - Given the admin selects a target or creates a new entity, when saved, then the association is updated locally and the record is queued for immediate retry. - Given no selection is made, when the admin closes the resolver, then the record remains in the Retry Queue and a reminder persists. - Given a wrong selection is made, when the admin chooses Undo within 10 minutes, then the association reverts and the record returns to unresolved state.
Bulk Remediation & Retry
- Given an admin views the Retry Queue, when they filter by category, record type, date range, owner, or campaign, then the list updates within 200 ms for up to 5,000 items. - Given the admin selects multiple items (up to 1,000), when they apply a bulk action (retry, assign campaign, set shift, mark do_not_contact where applicable), then the action is applied to all eligible items and ineligible items are reported with reasons. - Given a bulk action completes, when results are presented, then a summary shows total processed, succeeded, failed, and per-category counts, with a downloadable CSV for failures. - Given a bulk retry runs, when partial failures occur, then auto-retry/backoff is applied per record and the queue reflects per-record state without blocking others.
Observability, Notifications, and SLA Monitoring
- Given SyncAssign processes sync operations, when requests occur, then structured logs include operation type, record ID (hashed), user/role, category, attempt number, latency, result, and trace ID; PII is not logged. - Given the system runs, when metrics are scraped, then dashboards display p50/p95/p99 sync latency, success rate, error rate by category, retry depth, queue age, and notification delivery rate with ≤60s latency. - Given error rate by category exceeds 5% over 5 minutes or average queue age exceeds 15 minutes, when alerts evaluate, then on-call is paged and product owners receive a Slack/email alert with a link to the runbook. - Given a record requires human intervention (category mapping_ambiguity or consent_failure or >5 failed attempts), when threshold is reached, then the responsible user is notified via in-app notification and email within 2 minutes; notifications are deduplicated for 30 minutes and tracked to delivery.

Product Ideas

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

GapBuster Blitz

When no-shows hit, blast geotargeted SMS to nearby volunteers with one-tap claim links; auto-updates rosters and sends check-in directions. Fills last-minute gaps within minutes.

Idea

Coalition MergeGuard

Consent-aware deduping flags overlaps across partner lists, preserves “brought-by” attribution, and shares only approved fields. One-click safe merges keep coalition pipelines clean without turf wars.

Idea

ThermoText Receipts

SMS receipts include a live campaign thermometer and a one-tap “add $5” upsell. Micro-pledges convert instantly while excitement’s hot.

Idea

Access Prompt Cards

Before each shift, volunteers get a 3-tap SMS survey for accommodations; check-in shows clear badges like “ASL,” “seated,” or “scent-free.” Captains plan inclusively without scramble.

Idea

SeatSaver Auto-Relay

At T-10, no-shows trigger automatic reassignments and priority pings to alternates with one-tap join links. Keeps remote phonebanks at full strength.

Idea

PosterSafe Sharelinks

Generate time-limited, watermark-stamped Impact Board links with scoped metrics for sponsors and boards. Share confidently without exposing volunteer PII.

Idea

FlashKiosk QR

Instantly switch any phone into an offline kiosk with rotating QR codes for signups and gifts; auto-sync later and print sticky name labels on reconnect.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

GiveCrew launches mobile‑first nonprofit CRM to replace spreadsheets and scattered apps for grassroots teams

Imagined Press Article

Remote-first — August 9, 2025 — GiveCrew today announced the general availability of its lightweight, mobile‑first nonprofit CRM purpose‑built for grassroots organizers, part‑time staff, and rotating volunteers who operate without IT support. Designed to replace spreadsheets and scattered point solutions with one streamlined workflow, GiveCrew captures signups, donations, and shift assignments on any phone, auto‑sends receipts and reminders, and keeps open roles filled. Early pilot users cut administrative time by 58% while lifting volunteer show‑up by 22%, freeing scarce capacity to focus on impact instead of inboxes. Grassroots operations often move at the speed of text threads, pop‑up tables, and last‑minute shifts. GiveCrew meets teams where they work: in the field, on the go, and frequently offline. Tap‑first forms and offline language packs make it easy to collect clean data in moments, while a live Impact Board turns effort into visible progress that rallies teams and reassures stakeholders. “GiveCrew is the organizing system we wished we’d had: fast on a phone, forgiving in chaos, and respectful of people’s consent,” said Maya Ortiz, Communications Lead at GiveCrew. “We built for the real world of rotating volunteers, spotty service, and shared coalitions. When the tools get out of the way, people show up—and the Impact Board makes that momentum visible in real time.” GiveCrew unifies previously fragmented tasks into a single mobile workflow: - Capture: SmartRotate QR and icon‑forward prompts speed signups and small‑dollar gifts, even in dense crowds. DupShield quietly prevents spam and duplicate entries—even fully offline. - Assign: Captains can post shifts, view open roles, and use Claim Hold to prevent double‑booking as people respond by SMS or tap‑through links. Reliability Waves prioritize outreach based on history and proximity while rotating fairly to avoid burnout. - Confirm: Auto receipts, calendar links, and reminder sequences keep volunteers and donors on track without staff intervention. ETA Filter and “on the way” statuses reduce day‑of uncertainty. - Report: The live Impact Board aggregates progress without exposing PII, so executives, sponsors, and boards can see outcomes at a glance. Poster‑ready snapshots can be printed before meetings. For Campaign Architects, GiveCrew ships with templates that spin up events, forms, and automations in minutes. For Shift Captains, a mobile Captain Console shows open slots, incoming claims, ETAs, and map pins—with one‑tap controls to boost the radius or resend a wave. For Pop‑up Recruiters, Kiosk Lock turns any phone into a tamper‑proof sign‑up station with Offline Languages and LabelSpool for fast name badges that print on reconnect. And for Back‑Office Stewards, SyncAssign and automatic acknowledgments reconcile donations, maintain data hygiene, and post results to the Impact Board—no IT help required. “On paper, we were hosting weekly events. In reality, we were juggling spreadsheets, texts, and last‑minute changes,” said Rina Patel, a volunteer lead who piloted GiveCrew during a storm‑relief drive. “With GiveCrew, we stopped guessing. We knew who was coming, who needed accommodations, and which gaps to fill. I could stabilize the roster from my phone and show a real‑time poster to our sponsors. It felt like leveling up from duct tape to a real operations center.” GiveCrew is built for inclusive organizing from the start. Sticky Preferences auto‑remembers each volunteer’s last confirmed accommodations and pre‑fills a three‑tap SMS prompt for the next shift. Badge Glance surfaces clear, role‑aware indicators at check‑in so Captains can pre‑assign stations without last‑minute reshuffles. Access Routes send personalized arrival links with accessible entrances and quiet rooms, even when service is spotty. Need‑to‑Know Badges and Consent Ledger ensure only appropriate roles see sensitive details, honoring dignity and trust. Coalition work requires trust and clarity. GiveCrew’s Credit Keeper preserves partner attribution when volunteers fill each other’s gaps, while Scope Templates and HashMatch Keys enable privacy‑first list reconciliation. Merge Preview shows exactly how a proposed merge will look for each partner, and Attribution Shield keeps “brought‑by” credit visible across future updates and exports. A lightweight Dispute Inbox makes it simple to flag and resolve conflicts without side‑channel threads. “Most CRMs assume an office and a staff. Grassroots power is built by neighbors with phones,” said Daniel Cho, GiveCrew’s Head of Product. “We focused on fast, inclusive flows, consent‑aware data sharing, and a live feedback loop that motivates people to do the next small thing. That combination is what helps scrappy teams punch above their weight.” Availability and getting started GiveCrew is available today. New workspaces can start capturing signups and gifts in minutes, import existing lists, and post their first shifts without IT. The Impact Board ships with safe default scopes so leaders can share progress confidently from day one. Security and privacy Consent Ledger logs the who, what, when, and scope of every share and outreach channel. SafeAggregates protects privacy on shared views by suppressing small cells and disabling risky drill‑downs. Sponsors and boards can view progress via TimeLock Links, Watermark Shield, and Sponsor Gate, with ViewTrail auditing access and Snapshot Posters for meeting‑ready PDFs. Media kit and demo A media kit, product screenshots, and a short walkthrough video are available upon request. Live demos can be scheduled for organizing teams, coalitions, and funders. Press contact Press Contact: Maya Ortiz, Communications Lead Email: press@givecrew.org Phone: +1 (415) 555‑0139 Website: www.givecrew.org About GiveCrew GiveCrew is a mobile‑first nonprofit CRM for grassroots teams. It replaces spreadsheets and scattered apps with one workflow that captures signups, donations, and shift assignments, auto‑sends receipts and reminders, fills open slots, and powers a live Impact Board. Pilots cut admin time 58% and lifted volunteer show‑up 22%. Built for low‑connectivity environments and coalition work, GiveCrew is inclusion‑forward and consent‑aware by design.

P

GiveCrew unveils MergeGuard, a consent‑first data trust stack that ends coalition turf wars and protects volunteer privacy

Imagined Press Article

Remote-first — August 9, 2025 — GiveCrew today introduced MergeGuard, a consent‑aware data trust stack that lets coalition partners reconcile overlapping lists, preserve “brought‑by” credit, and share just the right fields—without exposing personally identifiable information (PII) or sparking turf wars. Built into GiveCrew’s mobile‑first nonprofit CRM, MergeGuard translates multi‑organization MOUs into safe, auditable data flows so that teams can focus on turnout and fundraising, not spreadsheets and side‑channel debates. Coalitions work when partners can collaborate in good faith—and falter when credit is lost, consent is unclear, or data privacy is compromised. MergeGuard addresses those failure points with a practical toolkit that works in the field and in the back office: - Consent Ledger: A time‑stamped, partner‑visible record of each contact’s consent, channel, and scope. Every merge and export checks this ledger first. - Scope Templates: Reusable, field‑level sharing templates that mirror MOUs, complete with allowed purposes and expiration dates. - HashMatch Keys: Privacy‑first matching using salted, rotating hashes of phone and email to find overlaps without exposing raw PII. - Merge Preview: A side‑by‑side record view with risk flags and a “result by partner” preview, plus instant undo. - Attribution Shield and Credit Keeper: Locks and visibly carries source credit across merges and future updates, keeping scorecards fair. - Dispute Inbox: A lightweight workflow to flag conflicts, comment, and propose resolutions; risky syncs auto‑pause until resolved. “Coalitions don’t fail on mission; they crack on mechanics,” said Daniel Cho, Head of Product at GiveCrew. “We built MergeGuard so organizers can safely combine efforts without losing trust. You can prove consent, preserve credit, and show exactly what each partner will see—before anything is finalized.” For Campaign Architects, MergeGuard compresses weeks of coordination into minutes. Pick a Scope Template that mirrors your MOU, preview the result by partner, and ship a safe sync that respects consent scopes. For Back‑Office Stewards, MergeGuard reduces risk: safe defaults, visible risk flags, and automatic suppression of small, sensitive counts in shared views. For Shift Captains and Pop‑up Recruiters, Credit Keeper ensures partner attribution carries through when volunteers fill open slots across organizations. “Before MergeGuard, our coalition spent more time reconciling lists than recruiting volunteers,” said Coalition Connector Camila, who leads a partner network across neighborhoods and campuses. “Now we can co‑host events with shared signups and clean attribution. Consent isn’t a spreadsheet—it's part of the workflow. That has changed the tone of our partnerships.” MergeGuard extends beyond the database into how impact is shared. TimeLock Links, Poster Scopes, and SafeAggregates ensure that Impact Board views are scoped to the right audience and timeline, while Watermark Shield and Sponsor Gate personalize and protect every sharelink. ViewTrail logs who viewed and downloaded what, with alerts for unusual activity and one‑tap revocation or scope tightening. Snapshot Posters produce moment‑in‑time, printer‑ready PDFs so leaders can walk into meetings with confidence. “Trust is the currency of coalition work. With MergeGuard and Impact Board protections, we can share progress without sharing PII—and still keep partners acknowledged for the people they mobilized,” said Aisha Khan, Operations Steward at a regional social services alliance. “We finally moved beyond email attachments and mystery spreadsheets.” Security and privacy by design MergeGuard aligns outreach and sharing with what people actually allowed. Every outbound list, SMS blast, or export is automatically checked against the Consent Ledger, blocking records that fall outside agreed scopes. HashMatch Keys enable overlap detection without exposing raw phone numbers or emails. SafeAggregates and Need‑to‑Know Badges prevent accidental re‑identification, even as teams drill into operations on event day. Inclusive and accessible collaboration GiveCrew’s inclusion‑forward foundation continues within MergeGuard workflows. Auto Language ensures partners and volunteers receive communications in their preferred language. Accessibility‑related fields respect Need‑to‑Know permissions by role, and Sticky Preferences carry accommodations across co‑hosted events when consent allows. Resource Match can forecast accommodation demand as joint signups roll in, helping teams stage interpreters, seating, or quiet rooms across venues. Availability and onboarding MergeGuard is rolling out to all GiveCrew workspaces starting today. Admins can enable consent scopes and partner templates in Settings and import existing MOUs for mapping. GiveCrew provides quick‑start templates and office hours for coalitions that want a guided setup. Media kit and demo Screenshots, a policy overview, and a technical whitepaper on HashMatch Keys are available upon request. GiveCrew offers live demos tailored to coalition operations and compliance teams. Press contact Press Contact: Maya Ortiz, Communications Lead Email: press@givecrew.org Phone: +1 (415) 555‑0139 Website: www.givecrew.org About GiveCrew GiveCrew is a mobile‑first nonprofit CRM for grassroots teams. It replaces spreadsheets and scattered apps with one workflow that captures signups, donations, and shift assignments, auto‑sends receipts and reminders, fills open slots, and powers a live Impact Board. Built for coalition collaboration, GiveCrew is consent‑aware and inclusion‑forward by design.

P

GiveCrew debuts Captain Console with Reliability Waves and ETA Filter to fill last‑minute shift gaps in minutes

Imagined Press Article

Remote-first — August 9, 2025 — GiveCrew today announced its turnout stabilization suite for event‑day operations, headlined by the new Captain Console and powered by Reliability Waves, Smart Radius, ETA Filter, Claim Hold, and Auto Language. The result is a fast, fair, and inclusive workflow that fills last‑minute gaps in minutes, reduces no‑shows, and keeps volunteers confident from the first ping to the final check‑in. Grassroots events rarely run on a tidy calendar. Schedules slip, weather turns, and people’s lives happen. Traditionally, that chaos becomes a scramble of texts, calls, and spreadsheets, with Captains trying to backfill positions while tracking who is truly on the way. GiveCrew’s turnout engine turns that scramble into a structured, mobile‑first flow that works in the field and with spotty service. The new Captain Console centralizes decision‑making on one screen: open slots, incoming claims, live ETAs, and map pins are visible at a glance. Captains can boost the radius or resend a wave with a tap, track which messages were sent in which language, and see who is en route. Claim Hold prevents double‑assignments as multiple volunteers tap links at once, auto‑releasing unconfirmed holds after a short window to keep rosters accurate in fast‑moving moments. Reliability Waves increase conversion while protecting the roster from burnout. Blitz messages go out in prioritized waves based on show‑up history, proximity, and availability tags, with fairness rotation to avoid pinging the same people every time. Combined with Smart Radius and ETA Filter, GiveCrew only contacts people who can realistically arrive before call time. Auto Language delivers each message in the volunteer’s preferred language with concise directions and a map link. “Before GiveCrew, we guessed and we hoped,” said Remote Roster Rahul, who coordinates distributed phonebanks. “Now, at T‑20 we send a ReadyCheck Ping and see who’s in. If a seat drops, the standby ladder fills it automatically. From my phone, I can confirm alternates, hand them a SeatPass, and watch ETAs settle. It feels like commanding a small air‑traffic control for volunteers.” The turnout suite integrates end‑to‑end with GiveCrew’s broader workflow: - ReadyCheck Ping: Two‑tap confirmations at T‑20/T‑15 surface no‑shows early and trigger standby backfill. - Standby Ladder: Pre‑ranked alternates opt‑in ahead of time; as T‑10 approaches, invites escalate until every seat is filled. - SeatPass Handoff: Alternates get a secure, expiring link that drops them into the right dialer seat or breakout room with the correct script pack, display name, and regional settings. - Instant Brief: A 60‑second micro‑brief gets alternates productive immediately, with a one‑tap test call and a “Ready” check. - Target Reflow and Timezone Triage: When someone drops mid‑shift, remaining targets reassign automatically within compliance windows and do‑not‑contact rules. “Fairness and clarity matter as much as speed,” said Daniel Cho, Head of Product at GiveCrew. “Reliability Waves balance urgency with equity, Smart Radius cuts noise, and ETA Filter reduces wasted outreach. Captains regain control, volunteers feel respected, and turnout stabilizes without heroics.” Accessibility and inclusion are built‑in. Sticky Preferences pre‑fill accommodations with a three‑tap SMS prompt. Badge Glance shows clear badges at check‑in—ASL, seated, low‑scent—so Captains can pre‑assign stations and avoid last‑minute reshuffles. Access Routes send personalized arrival links that include accessible entrances and quiet rooms, and Need‑to‑Know Badges ensure only the right roles see sensitive information. Resource Match forecasts accommodation demand from incoming prompts and suggests chairs, interpreters, or alternate assignments as needed. The turnout tools also respect coalition credit and consent. Credit Keeper preserves “brought‑by” attribution when partners fill each other’s gaps, and Consent Ledger ensures that outreach honors the scope people actually allowed. Merge Preview shows how a proposed roster merge will appear to each partner, and Dispute Inbox offers a lightweight path to resolve conflicts with SLA tracking. “On a busy Saturday, the difference between a full roster and a faltering event is minutes,” said Marisol Vega, a Shift Captain with a citywide mutual aid network. “GiveCrew took us from anxious group chats to a calm cockpit. People got messages they could act on, in their language, with real ETAs. We ended the day with smiles and a clean report to our board.” Availability and getting started The Captain Console and associated turnout features are available today in GiveCrew. Captains can enable Reliability Waves and ETA Filter from the event’s Outreach tab, set quiet hours, and define fairness rules. A guided setup helps teams configure Smart Radius defaults and language packs. Media kit and demo A recorded walkthrough of the Captain Console, along with configuration guides and case studies, is available upon request. GiveCrew offers live demos tailored to event operations teams and distributed phonebank programs. Press contact Press Contact: Maya Ortiz, Communications Lead Email: press@givecrew.org Phone: +1 (415) 555‑0139 Website: www.givecrew.org About GiveCrew GiveCrew is a mobile‑first nonprofit CRM for grassroots teams. It replaces spreadsheets and scattered apps with one workflow that captures signups, donations, and shift assignments, auto‑sends receipts and reminders, fills open slots, and powers a live Impact Board. Built for low‑connectivity environments and coalition work, GiveCrew is inclusion‑forward and consent‑aware by design.

P

GiveCrew adds ThermoText receipts with one‑tap top‑ups to multiply small‑dollar gifts and sustain momentum

Imagined Press Article

Remote-first — August 9, 2025 — GiveCrew today launched a micro‑fundraising upgrade that turns routine donation receipts into high‑conversion, one‑tap moments. ThermoText Receipts now include a live campaign thermometer and a personalized “add $5” upsell powered by Smart Step‑Up, with TapToken Checkout, Match Minute, HeatBurst Meter, Later Nudge, and Share Sprint working behind the scenes to turn excitement into immediate impact. Grassroots fundraising is powered by small, timely actions. Traditional CRMs bury donors in redirects and forms, losing momentum and trust. GiveCrew’s mobile‑first approach delivers a frictionless, consent‑aware experience on the device people use most: their phone. Donors see their gift count instantly, get a clear path to do a little more, and can spread the word with a single tap. “Micro‑pledges are the heartbeat of grassroots campaigns,” said Text‑to‑Give Talia, a roving fundraiser who participated in early testing. “ThermoText Receipts showed donors the jump their $5 made in real time, and the one‑tap top‑up felt effortless. People loved seeing the bar move and sharing that moment with friends.” The new toolkit works together to raise more without fatiguing supporters: - Smart Step‑Up: Personalizes the one‑tap upsell amount and message based on donor history, current gift, and urgency. It quietly A/B tests copy in the background and suggests the most likely‑to‑convert add‑on—such as +$3, +$5, or +$10. - TapToken Checkout: Enables true one‑tap top‑ups for repeat donors using a previously consented, secure payment token. First‑time donors see a fast mobile wallet fallback. - Match Minute: When matching funds are live, receipts automatically show a time‑bound banner—“Your next $5 becomes $10 for 20 minutes”—and disable themselves when the cap is reached. - HeatBurst Meter: Turns the live thermometer into tiny milestones like “5 donors to 75%,” celebrating the moment someone pushes the bar past a marker. - Later Nudge: Offers quick‑reply deferrals inside the receipt—“Later today,” “Tomorrow,” “This weekend”—and auto‑schedules the same one‑tap upsell at the chosen time, honoring quiet hours and stopping after a decline. - Share Sprint: After a top‑up, donors get a one‑tap forward link with prefilled text and a personal mini‑goal—“I’m rallying 5 friends to add $5.” The shared link includes a tiny progress meter to turn donors into micro‑ambassadors. “Small‑dollar giving is about trust and timing,” said Daniel Cho, Head of Product at GiveCrew. “We cut the friction to near zero and made the impact visible. The toolkit is respectful, consent‑aware, and designed for repeat micro‑actions across the week—not pressure.” For Campaign Architects, setup is fast. Pick an upsell strategy with Smart Step‑Up, enable Match Minute rules when a sponsor posts a match, and add a HeatBurst milestone schedule to your campaign. For Back‑Office Stewards, receipts and acknowledgments stay compliant by design. Every top‑up is tracked, donors can update preferences in a tap, and tax receipts remain accurate with consolidated records. ThermoText Receipts integrate with GiveCrew’s broader organizing workflow. Pop‑up Recruiters can accept small‑dollar gifts at doors, tables, or rallies using SmartRotate QR codes that adapt to crowd density. DupShield filters junk entries even offline, and SyncAssign posts progress to the live Impact Board on reconnect. Sponsors and boards can safely view fund‑raising progress via TimeLock Links and Poster Scopes, protected by Watermark Shield and Sponsor Gate and audited with ViewTrail. Accessibility and inclusion are standard. Auto Language delivers receipts in each donor’s preferred language with concise, accessible directions. Large‑type and RTL language packs ensure the experience works on low‑end devices and for more donors. Sticky Preferences carry forward communication choices so supporters see the right prompts at the right time and can opt out easily. “Every $5 matters, especially in the last mile to a goal,” said Alicia Romero, Development Director at a youth leadership nonprofit using GiveCrew. “We used to blast a link and hope. Now, every receipt is a moment to celebrate, invite a tiny step‑up, or turn a donor into a messenger. The difference shows up on our Impact Board—and in our programs.” Availability and getting started ThermoText Receipts and the associated micro‑fundraising tools are available today in GiveCrew. Campaigns can enable Smart Step‑Up and TapToken in Settings, define Match Minute windows, and configure HeatBurst milestones per thermometer. A quick‑start guide and message templates are included. Privacy and security TapToken Checkout requires explicit donor consent and supports easy revocation. Consent Ledger records preferences and scope changes, while SafeAggregates protect privacy in shared fundraising views. GiveCrew honors quiet hours and opt‑outs automatically across Later Nudge and Share Sprint. Media kit and demo Sample receipts, a configuration walkthrough, and a sponsor‑ready overview for matching campaigns are available upon request. GiveCrew offers live demos for development teams and finance stewards. Press contact Press Contact: Maya Ortiz, Communications Lead Email: press@givecrew.org Phone: +1 (415) 555‑0139 Website: www.givecrew.org About GiveCrew GiveCrew is a mobile‑first nonprofit CRM for grassroots teams. It replaces spreadsheets and scattered apps with one workflow that captures signups, donations, and shift assignments, auto‑sends receipts and reminders, fills open slots, and powers a live Impact Board. Pilots cut admin time 58% and lifted volunteer show‑up 22%. Built for low‑connectivity environments and coalition work, GiveCrew is inclusion‑forward and consent‑aware by design.

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.