HOA management software

Duesly

One Feed. On-Time Dues.

Duesly is a lightweight HOA management platform for volunteer boards and part-time managers at small and mid-size communities. It merges announcements, payments, and compliance into one clean feed. Turn any post into a bill with one click, trigger automated, logged reminders, and replace email chaos and paper checks—lifting read rates and on-time dues.

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

Duesly

Product Details

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

Vision & Mission

Vision
Transform every HOA into a calm, transparent community where communication flows effortlessly and dues are paid on time.
Long Term Goal
By 2029, power 25,000 associations worldwide to 90% on-time dues and 90% message reach, while halving board admin hours across communities.
Impact
For volunteer HOA boards and part-time managers, Duesly cuts late payments by 60% within two cycles, reduces follow-up work by 8 hours/month, and lifts announcement read rates to 85%, while documenting 100% of rule notices and driving 40% fewer homeowner complaint emails.

Problem & Solution

Problem Statement
Volunteer HOA boards at small and mid-size associations juggle announcements, violations, and dues across email, paper mail, porch chats, and spreadsheets, driving late payments and friction. Existing tools are bloated, lacking integrated click-to-pay posts with automated, logged reminders.
Solution Overview
Duesly replaces scattered emails and spreadsheets with a single feed where board posts become bills. One-click Bulletin-to-Bill enables instant online payment, while automated, logged reminders escalate overdue notices—cutting manual follow-ups and boosting on-time dues without arguments or paper checks.

Details & Audience

Description
Duesly is a lightweight HOA management platform that unifies announcements, payments, and compliance into one clean feed. Built for volunteer boards and part-time managers in small to mid-size associations. It replaces email chaos, manual follow-ups, and late checks with instant, automated online payments and logged reminders. Bulletin-to-Bill turns any board post into a payable item with one click, boosting on-time dues and accountability.
Target Audience
Volunteer HOA board members and part-time managers (35-65) at small/mid-size associations, seeking on-time dues; conflict-averse.
Inspiration
On a Saturday in the condo clubhouse, the treasurer slid four battered folders across a folding table—bounced checks, printed emails, sticky notes, violation slips. A late-fee dispute swelled into a neighbor shouting match while she hunted a lost message. It hit me: the announcement and the ask lived apart. If one clean post could also be the bill—with automated, logged reminders—the tension and paper trail could vanish.

User Personas

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

R

Remote Landlord Riley

- Owns two condos; lives 300+ miles from both communities. - Mid-career professional; frequent travel; self-manages tenants. - Uses iPhone; prefers mobile banking and e-signatures. - Coordinates with accountant quarterly for rental filings. - Time zone mismatch with community office hours.

Background

Missed notices once led to a tenant’s amenity suspension and late fees. Built a spreadsheet workaround but still lost track across inboxes. Now prioritizes tools that centralize payments, notices, and documentation for remote clarity.

Needs & Pain Points

Needs

1. Autopay with instant receipts and ledger details. 2. Real-time balance, fines, and notice visibility. 3. Easy tenant-forwarding controls for announcements.

Pain Points

1. Notices lost in owner-tenant email loops. 2. Paper checks delay posting and deposits. 3. No clear trail for violations or fines.

Psychographics

- Delegates by default; demands dashboard-level clarity. - Trusts automation when receipts are bulletproof. - Values time savings over feature bloat. - Pragmatic, not nostalgic about legacy processes.

Channels

1. Duesly Mobile - push alerts 2. Email - receipts 3. SMS - urgent dues 4. WhatsApp - tenant relay 5. Google Drive - documents

C

Committee Coordinator Casey

- Lives on-site; leads social or amenities committee. - Works in marketing/operations; comfortable with basic design tools. - Heavy mobile user; balances day job and evening volunteering. - Organizes 6–12 events per year across seasons.

Background

Grew tired of planning via fragmented Facebook groups and reply-all emails. Built ad-hoc Google Forms for RSVPs, but results scattered. Seeks one feed for invites, reminders, and recap posts.

Needs & Pain Points

Needs

1. Event templates with RSVPs and reminders. 2. Scheduled posts and pinned summaries. 3. Photo updates and quick polls.

Pain Points

1. Low turnout from missed, scattered invitations. 2. Duplicated info across platforms; version confusion. 3. No central calendar or recap visibility.

Psychographics

- Community pride; loves visible participation wins. - Organizing energy; zero tolerance for chaos. - Data-curious; wants turnout trends. - Friendly tone, low-drama communication style.

Channels

1. Duesly Mobile - feed posts 2. Email - invitations 3. Facebook Groups - chatter 4. Canva - graphics 5. Nextdoor - neighborhood spillover

A

ARC Applicant Aria

- Owns single-family home or townhome in covenant community. - DIY-inclined; coordinates with local contractors. - Android user; takes site photos on phone. - Recently purchased; eager to personalize within guidelines.

Background

Had a deck plan delayed after submitting incomplete documents via email. Learned requirements vary by project type and reviewer. Now seeks guided submissions with validations and visible status changes.

Needs & Pain Points

Needs

1. Step-by-step ARC forms with file validations. 2. Real-time status and reviewer comments. 3. Reference examples and requirements by category.

Pain Points

1. Opaque status and changing requirements. 2. Lost attachments and duplicate requests. 3. Costly delays from avoidable resubmissions.

Psychographics

- Rule-respecting, allergic to bureaucracy. - Clarity seeker; prefers checklists over guesswork. - Impatient with repeat requests. - Values fair, consistent decisions.

Channels

1. Duesly Mobile - ARC submissions 2. Email - status updates 3. Google Photos - attachments 4. YouTube - how-tos 5. SMS - approvals

A

Audit-Ready Accountant Avery

- CPA or certified bookkeeper; 5–10 HOA clients. - Works remotely; standardized QuickBooks Online workflows. - Contract-based; bills per close or engagement. - Advanced Excel user; automation-minded.

Background

Spent years chasing treasurers for statements and check images. Built import macros but hit mismatched categories and missing memos. Prioritizes systems that deliver repeatable, complete data packages.

Needs & Pain Points

Needs

1. Scheduled CSV exports with stable schemas. 2. Payment details with payer, memo, method. 3. Aging and variance reports by period.

Pain Points

1. Missing fields break imports and reconciliations. 2. Inconsistent categories across communities. 3. Manual tie-outs to bank statements.

Psychographics

- Precision over pace; but loves both together. - Documentation zealot; audit trails are nonnegotiable. - Automation-first; manual entry is failure. - Change-friendly if standards are met.

Channels

1. QuickBooks Online - reconciliation 2. Duesly Web - exports 3. Email - monthly packets 4. Google Drive - shared folders 5. Slack - client threads

M

Multilingual Member Mateo

- Bilingual household; Spanish-first reading preference. - Works variable shifts; checks phone during breaks. - Android user; limited desktop access. - Lives with family; shares updates via WhatsApp.

Background

Missed dues and pool updates due to English-only emails and cluttered threads. Found push notifications far more reliable than newsletters. Seeks language-inclusive, low-friction communication.

Needs & Pain Points

Needs

1. Auto-translated posts with simple summaries. 2. SMS links for one-tap payments. 3. Visual due dates and receipts.

Pain Points

1. English-only notices reduce comprehension. 2. Email clutter hides urgent items. 3. Password friction on quick tasks.

Psychographics

- Inclusion-minded; detests jargon and legalese. - Skims fast; responds to clear visuals. - Trust grows with consistent reminders. - Family-forward information sharing.

Channels

1. Duesly Mobile - Spanish interface 2. SMS - payment links 3. WhatsApp - household sharing 4. Email - receipts 5. Facebook - group updates

P

Paper-Lite Payer Priya

- Longtime resident; historically mailed checks with memos. - Moderate tech confidence; prefers tablet over desktop. - Values printed confirmations; keeps organized home files. - Handles household finances; pays bills on schedule.

Background

A lost check created late fees and strained neighbor relations. Tried a bank bill-pay, but reference fields didn’t map. Now open to digital if proof and support are obvious.

Needs & Pain Points

Needs

1. Guided onboarding with test $0.00 flow. 2. Instant, printable receipt history. 3. Clear refund and dispute steps.

Pain Points

1. Fear of double charges or wrong accounts. 2. Confusing save-card/bank wording. 3. Hard-to-find receipts after payment.

Psychographics

- Risk-averse; proof beats promises. - Learns by doing with guidance. - Prefers humans nearby if stuck. - Security cues strongly influence trust.

Channels

1. Duesly Web - guided setup 2. Email - confirmations 3. SMS - gentle reminders 4. Phone Support - reassurance 5. Community Meeting - demos

Product Features

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

Smart Language Detect

Automatically sets each member’s translation language from device settings and past behavior, with a one‑tap override per post. Reduces setup and ensures messages arrive in the language they actually read.

Requirements

Device Locale Auto-Detection
"As a community member, I want the app to automatically use my device language so that I can read posts without configuring settings."
Description

Automatically detect a member’s preferred language from device/app locale and browser Accept-Language on first use and sign-in, map to Duesly’s supported languages, and persist the selection on the member profile. Apply a clear fallback order (explicit user setting > behavior model > device locale > community default > platform default) and synchronize the chosen language across channels (feed, push, email). Respect an admin or user lock that prevents auto-changes, surface a lightweight in-app notice when the language is auto-set, and ensure no sensitive data beyond the language code is stored. Provide a mapping table for locale variants (e.g., es-419) and gracefully handle unsupported locales.

Acceptance Criteria
Auto-Detect on First Use and Sign-In
Given a member has no explicit language setting and no language lock And the member launches the mobile app for the first time or signs in on the web When the system reads the device/app locale (mobile) or the browser Accept-Language header (web) Then the member’s preferred language is selected based on the detected locale mapped to a supported language And the selected language code is persisted on the member profile And the UI renders subsequent screens in the selected language without additional user action
Locale Mapping and Variant Handling
Given the detected locale is a regional or script variant (e.g., es-419, pt-PT, zh-Hant-TW) When the system maps the locale to Duesly’s supported languages Then it selects the exact variant if supported And if the exact variant is unsupported, it falls back to the parent language (e.g., es-419 -> es) if supported And if neither are supported, it proceeds with the defined fallback order without surfacing an error to the user And the final chosen language is used for rendering
Fallback Order Enforcement
Given multiple potential language sources exist for a member When determining the member’s effective language Then the system applies the priority: explicit user setting > behavior model prediction > device/app or browser locale > community default > platform default And if an explicit user setting exists, it is always used And if no explicit setting exists but a behavior model prediction exists, it is used regardless of device locale And if neither exist, the device/app or browser locale is used when supported, else the community default, else the platform default
Cross-Channel Language Synchronization
Given a member has an effective language selected When the system delivers content via feed UI, push notifications, and email Then all channels use the same effective language for that member And if a translation is missing in a channel, that channel falls back to community default then platform default while other channels continue using the member’s effective language And changes to the member’s effective language propagate to all channels within one minute
Admin/User Language Lock Respect
Given the member profile has the language lock enabled by either the member or an admin When the device/app locale changes, the behavior model predicts a different language, or the member signs in again Then the system must not change the stored language automatically And auto-detection and auto-updates are bypassed while the lock is active And the UI continues in the locked language until the lock is removed by an authorized action
In-App Notice on Auto-Set Language
Given the system auto-sets or auto-updates a member’s language and no language lock exists When the change is applied Then a lightweight in-app notice (banner/toast) is shown once per change, localized in the selected language, explaining the selection and providing a link to change language And the notice is dismissible and is not shown again for the same language unless the language changes later And the notice does not block primary actions and disappears automatically after a reasonable timeout (e.g., 5–7 seconds) if not interacted with
Privacy and Data Minimization
Given the system detects and persists a member’s language Then only the language code (ISO 639 with optional region, e.g., "es" or "pt-BR") is stored on the member profile for this feature And the raw Accept-Language header, full locale strings, and other personal data are not persisted And application and server logs redact full headers and store at most the resulting language code And automated privacy checks flag any attempt to store additional sensitive data for this feature
Behavior-Based Language Adaptation
"As a community member, I want the app to learn my reading language from my actions so that messages arrive in the language I actually read."
Description

Continuously learn a member’s reading preference from explicit one-tap overrides and confirmed reading behavior to adjust the default language automatically. Use transparent heuristics (e.g., three consecutive overrides to the same language within 30 days triggers a preferred-language update) with a cooldown period and opt-out. Never override an explicit user-locked setting. Log adaptation events with reason codes, allow rollback to the previous language, and expose a concise explanation to the user (“We switched your language based on your recent choices”). Ensure data minimization and retention limits for any behavioral signals used.

Acceptance Criteria
Preferred Language Update After Consecutive Overrides
Given a member’s default language is L0 And the member performs three consecutive one‑tap overrides to the same language L1 within a rolling 30‑day window And each overridden post has a confirmed read event When the third confirmed read is recorded Then the system updates the member’s preferred language to L1 And sets L1 as the default for future posts and notices And records the adaptation timestamp
Cooldown After Language Adaptation
Given an automatic preferred‑language update occurred at time T0 When additional overrides or reads occur within 14 days after T0 Then no further automatic preferred‑language update is applied And the consecutive‑override streak is tracked without triggering a change And after T0 + 14 days, normal heuristic evaluation resumes (default cooldown = 14 days, configurable)
Respect User-Locked Language Setting
Given a member has explicitly locked their language to Lx in Settings When overrides or behavioral signals meet any adaptation heuristic Then the system does not change the preferred language And no adaptation notice is shown And adaptation evaluation is suspended until the lock is removed
Member Opt-Out of Behavior-Based Adaptation
Given a member has opted out of behavior‑based language adaptation When overrides or behavioral signals would otherwise qualify for an automatic update Then no automatic preferred‑language change occurs And no adaptation notice is shown And the opt‑out state persists across devices and sessions And upon opting back in, heuristic evaluation restarts with streak counters cleared
Adaptation Event Logging With Reason Codes
Given any automatic preferred‑language change is applied Then the system logs an adaptation event containing: pseudonymous member_id, old_language, new_language, trigger_type (e.g., "three_overrides_30d"), evidence_count, timestamp, app_version, reason_code And the log is retrievable via authorized admin/audit APIs And no message content is stored in the event
Explanation Notice and One-Tap Rollback
Given an automatic preferred‑language change is applied When the member next opens the app or web within 7 days Then a non‑blocking notice is shown stating "We switched your language based on your recent choices" and naming the new language And the notice offers a one‑tap Revert action that immediately restores the previous language and records a rollback event with reason_code "user_revert" And the notice is shown at most once per adaptation per device And dismiss or revert actions are recorded
Data Minimization and Retention Limits
Given behavioral signals are processed for language adaptation Then only the following are stored for this purpose: language override events (language_code, timestamp), confirmed read flags (boolean, timestamp), and counters; no message content, IP addresses, or full device identifiers are stored And raw behavioral signals are retained for a maximum of 90 days then permanently deleted or irreversibly aggregated And adaptation/rollback audit logs are retained for 1 year then deleted And all processing respects member opt‑out and user‑locked language states
Per-Post One-Tap Override
"As a community member, I want to switch the language of a post with one tap so that I can quickly read it in my preferred language."
Description

Add a prominent, accessible language switcher on each post and announcement that allows a one-tap change to the desired language with instant re-rendering of content and metadata. Remember the choice contextually (for that post/thread) and optionally prompt to apply it as the global preference. Support offline retry for translation fetches, show loading and fallback states, and log override events for behavior learning. Ensure the control is keyboard and screen-reader accessible and works consistently across mobile and web.

Acceptance Criteria
Immediate In-Post Language Switch
Given a user is viewing a post or announcement with the language switcher visible When the user selects a different language from the switcher Then the UI enters a translating state within 150 ms And if the target translation is cached, the post content and translatable metadata render in the target language within 200 ms And if the translation must be fetched, final translated content renders within 2 seconds p95 (5-second absolute timeout) on a healthy connection And the switcher reflects the selected language, no full page/screen reload occurs, and scroll position is preserved
Post-Scoped Memory and Optional Global Prompt
Given the user overrides the language on a post or thread Then that post or thread re-opens in the chosen language for the same user for 30 days or until changed again And on the first override per session, show a non-blocking prompt "Apply to all posts?" with options Apply and Dismiss When the user chooses Apply, the account’s global language preference is updated and used as default on all future posts across devices within 60 seconds When the user chooses Dismiss or closes the prompt, no global change occurs and the prompt is not shown again for 24 hours And the prompt is focus-trappable, dismissible via Esc, and does not obscure primary content on small screens
Offline Retry and Fallback States
Given the user triggers a translation while offline or the translation service times out or fails Then the original-language content remains visible with a visible "Showing original—translation pending" indicator And a retry queue is created with exponential backoff (1s, 2s, 5s, 10s, 30s; max 5 attempts) and persisted for 24 hours And the user can manually Retry from the switcher; manual retries do not count toward backoff When connectivity is restored or a retry succeeds, the translated content replaces the original within 300 ms and the indicator clears And after 5 failed attempts or 24 hours, the UI shows a non-blocking "Translation unavailable" message with a link to help
Keyboard and Screen-Reader Accessibility
Given keyboard-only navigation Then the language switcher is reachable in logical tab order, supports Enter or Space to open and select, exposes role="button" or role="menu" with appropriate aria-haspopup and aria-expanded, and shows visible focus And each option has an aria-label that includes the language name and selection state; screen readers announce on change (e.g., "Translated to Spanish") And color contrast for text and icons is at least 4.5:1; tap or click targets are at least 44x44 px or dp; control is operable with 200% text scaling And focus remains on the switcher after content re-render; no unexpected focus loss; a live region with aria-live="polite" announces successful translation
Override Event Logging and Delivery
Given a language override action Then an analytics event is recorded with userId (hashed), postId or threadId, platform, fromLanguage, toLanguage, trigger (tap or keyboard), timestamp (UTC), result (success, fallback, or offline-queued), and latency in milliseconds And events are deduplicated per action id, stored offline, and flushed within 60 seconds on network availability And 95% of events are delivered to the analytics sink within 5 minutes; failures are retried with backoff; no raw personally identifiable information (e.g., names or emails) is sent
Cross-Platform Parity (Mobile and Web)
Given the feature on iOS 16+, Android 10+, and modern web (latest two versions of Chrome, Safari, Firefox, and Edge) Then the switcher’s placement, labels, available language list, and behaviors are consistent across platforms And orientation changes, dynamic type or font scaling, and responsive breakpoints do not clip or overlap the control And end-to-end tests verify identical outcomes for the same actions across platforms, including offline and retry flows
Translation Coverage for Content and Metadata
Given an override to a target language Then the post or announcement body, title, badges, call-to-action labels (e.g., Pay Now), system notices, and date, time, and number formatting render in the target locale And user names, payment amounts, and proper nouns are not machine-translated; currency and date formats localize without altering stored values And dynamic metadata (e.g., "Due in 3 days", "2 comments") updates to the target language with correct pluralization rules
Translation Provider Abstraction and Cache
"As a board admin, I want translations to be fast, accurate, and cached so that members get instant, consistent translations at low cost."
Description

Introduce a provider-agnostic translation layer that supports multiple translation engines (e.g., DeepL, Google) with configurable routing and failover. Implement a translation memory that caches post translations by post ID, version, and language, with invalidation on edits. Allow admin review and manual edits to machine translations, with version control. Include rate limiting, batching, and cost tracking, and ensure consistent terminology via optional glossaries. Provide deterministic fallbacks when a provider fails and return clear error states to the UI.

Acceptance Criteria
Provider Routing and Deterministic Failover
Given a routing configuration prioritizing DeepL over Google for en→es And a health policy defining timeout=3s and maxRetries=1 per provider When a translation request for en→es is submitted Then the system attempts DeepL first and uses Google only if DeepL fails deterministically (timeout, 5xx, or rate-limit) And the selected provider order is reproducible across identical requests And the response includes providerUsed, failoverOccurred (true/false), and attemptCount And logs capture provider result codes and failoverReason with a correlationId And no duplicate billing occurs for the same text segment during failover (idempotency enforced)
Translation Memory Cache Hit by Post/Version/Language (and Glossary)
Given the translation memory contains a translation for postId=PID, postVersion=V, targetLanguage=LANG, and glossaryVersion=G (or none) And the stored contentHash matches the post’s current content When a client requests translation for PID/V/LANG with glossaryVersion=G Then the cached translation is returned without calling any external provider And cache=true and cacheKey fields are present in the response And cacheHit metric increments by 1 And p95 server-side latency for cache hits is ≤150ms under nominal load
Cache Invalidation on Post Edit
Given a post with existing cached translations for version V When the post content is edited and saved as version V+1 Then all cache entries keyed to version V remain readable for auditing but are not eligible for lookups for version V+1 And the first translation request for version V+1 triggers a provider call and seeds new cache entries keyed to V+1 And subsequent requests for V+1 return the new cached translations
Admin Review, Manual Edit, and Version Control of Translations
Given an admin opens the translation review UI for postId=PID, version=V, language=LANG When the admin edits the machine translation and saves Then the system stores a new translationRevision with author, timestamp, and diff from machine output And the admin-edited revision becomes the default served translation for PID/V/LANG And prior revisions remain retrievable with full audit trail And the admin can revert to any prior revision, after which that revision becomes the default And cache is updated to serve the chosen revision on subsequent requests
Rate Limiting, Batching, and Cost Tracking Across Providers
Given provider configs define QPS, burst, dailyQuota, maxBatchChars, and pricing per character When 100 translation requests for the same provider/language pair arrive within 1 minute Then requests are batched up to maxBatchChars and dispatched without exceeding QPS or dailyQuota And no provider returns HTTP 429 in integration tests with configured limits And overflow requests are queued and retried with exponential backoff (jittered) up to maxRetries And per-request and aggregated cost are recorded with provider, communityId, postId, language pair, characters billed, unitPrice, and currency And cost totals reconcile to provider invoices within ±1% in reconciliation tests
Glossary Application for Consistent Terminology
Given a glossary is configured for en→es with version G containing term mappings When a translation is requested for en→es with glossaryVersion=G Then a provider that supports glossaries is selected per routing rules, or fallback proceeds without glossary if none support it And translations contain the exact glossary terms for all applicable entries And the response includes glossaryApplied=true/false and glossaryVersion And the cache key includes glossaryVersion so changes to the glossary do not reuse older cached entries And glossaryApplication metric counts applied terms per request
Clear Error States and Deterministic Fallback Responses to UI
Given all providers fail for a translation request and no cached translation exists When the request concludes after configured retries Then the API returns HTTP 503 with code=TRANSLATION_PROVIDER_UNAVAILABLE, a human-readable message, correlationId, and retryAfter And if a stale cached translation exists, the API returns HTTP 200 with stale=true and includes lastUpdated timestamps And if primary fails but a fallback succeeds, the API returns HTTP 200 with providerUsed set to the fallback and failoverOccurred=true And error and fallback events are logged with structured fields suitable for alerting
Admin Language Policy Controls
"As a board admin, I want to control default and allowed languages and when auto-detect applies so that communications meet policy and compliance needs."
Description

Provide an admin console to configure community-level language policies: set the default language, allowed languages, auto-detect on/off, and fallback order. Enable language locks for critical or compliance posts, and allow per-post overrides (e.g., force bilingual delivery). Support custom glossaries and preferred terms, and expose a test tool to preview content in selected languages. Audit all policy changes with timestamps and actor IDs. Ensure these policies propagate to the feed, notifications, and billing posts generated from announcements.

Acceptance Criteria
Community Language Policy Configuration
Given I am a Community Admin with access to Language Policies When I set a Default Language from the supported list And I select one or more Allowed Languages (including the Default Language) And I define a Fallback Order with unique, supported languages And I toggle Auto-Detect on or off And I save the policy Then the policy saves successfully and is displayed identically on reload of the console and via the Policies API And validation prevents saving if Allowed Languages is empty, Default Language is not in Allowed Languages, or Fallback Order contains duplicates or unsupported languages
Policy Propagation to Feed, Notifications, and Billing
Given Auto-Detect is enabled and the Default Language and Fallback Order are configured When a new announcement is published after the policy is saved Then each member’s delivery language is chosen by device settings/past behavior when available And if no detection is available, the Default Language is used; otherwise the next language in the Fallback Order is used And the same selected language is applied to the feed item, email, push/SMS notifications, and any billing post generated from the announcement
Critical/Compliance Post Language Lock Precedence
Given community language policies are configured and Auto-Detect may be on When an author marks a post as Critical/Compliance and applies a Language Lock selecting specific language(s) Then recipients receive the post and all related notifications only in the locked language(s) And member-side language overrides are disabled for that post And the lock takes precedence over Auto-Detect, Default Language, and Fallback Order
Per-Post Override for Forced Bilingual Delivery
Given a standard post is being composed When the author enables Per-Post Language Override and selects two languages (e.g., English and Spanish) Then recipients receive bilingual delivery containing both language variants in the same feed item and notifications regardless of Auto-Detect status And any bill generated from the post produces bilingual artifacts that include both selected languages
Custom Glossary Application and Precedence
Given a community glossary with preferred terms and translations exists When content is translated for delivery Then glossary terms are applied consistently across feed, notifications, and billing outputs And glossary substitutions override machine translation for matching terms And when a glossary entry is updated, subsequent translations reflect the change while previously delivered messages remain unchanged
Language Preview Tool Accuracy
Given I open the Language Preview tool in the admin console When I input sample content and choose one or more target languages Then each preview displays the exact text that would be delivered, honoring the current policy (Auto-Detect setting, Default Language, Fallback Order), any per-post overrides/locks, and the community glossary And each preview indicates the language source reason (Detected, Default, Fallback, Lock, Override)
Audit Trail for Policy and Glossary Changes
Given an admin changes any community language policy or glossary entry When I open the Language Policy Audit view or query the Audit API Then an audit record exists for each change including timestamp and actor ID And the audit record identifies the changed entity and action (create/update/delete)
Accessibility, Formats, and RTL Support
"As a community member, I want translated content to display correctly in my language and script so that it is legible and accessible across the app."
Description

Ensure translated content renders correctly for right-to-left scripts and locale-specific typographic rules, including mirroring UI affordances where necessary. Localize dates, numbers, currency, and pluralization in translated views. Translate alt text for images and captions for attachments where available, and provide graceful fallbacks when text cannot be extracted. Maintain AA contrast for language switch controls and support screen readers with proper language tags. Keep notification languages consistent with in-app views.

Acceptance Criteria
RTL Post Rendering and UI Mirroring
Given a member’s preferred language is RTL (ar, he, fa, ur) and a translated post is displayed in the feed When the view renders the post and surrounding UI Then the post container has dir="rtl" and bidi isolation for embedded LTR substrings/spans (dir="ltr" or unicode-bidi: isolate) And UI affordances (back chevron, reply arrow, pagination arrows, disclosure icons, avatar/timestamp alignment) are mirrored for RTL And no horizontal overflow occurs at viewport widths 320px, 768px, and 1280px And numerals and punctuation render legibly with locale-appropriate shaping And automated visual snapshot tests for RTL pass with zero unexpected diffs
Locale Formatting for Dates, Numbers, Currency, and Plurals
Given the app locale is one of {en-US, fr-FR, es-ES, ru-RU, ar-EG} When rendering translated views that include dates, numbers, and currency Then dates use CLDR patterns for the locale (short and relative time) And numbers use locale-specific grouping and decimal separators And currency amounts display with correct symbol, placement, and spacing per currency and locale And pluralized strings resolve to the correct category for counts 0,1,2,3,5,11,101 per locale rules And unit tests assert expected formatted outputs for the sample set above
Image Alt Text Translation with Fallbacks
Given a translated post contains images When assistive technologies query alternatives for each image Then non-decorative images expose translated alt text in the target language and lang attributes match the alt language And if translation fails, the original alt text is exposed with correct lang and a "View original" control is available And if no author-provided alt exists, a localized fallback "Image: {filename}" is provided; decorative images have empty alt or aria-hidden And accessibility audit shows 0 images with empty/null alt where role is informative
Attachment Caption Translation and Text Extraction Fallbacks
Given a post includes attachments (PDF, DOCX, CSV) with captions or extractable text When the translated view renders attachments Then captions are translated and exposed to screen readers as accessible descriptions And if text extraction or translation fails, the UI shows a localized fallback "Attachment: {filename} ({type}, {size})" and preserves accessible download controls And an error event (no PII) is logged with attachment id and locale on extraction/translation failure And a "View original language" toggle is shown when a caption is machine-translated
Language Switch Control Contrast and Operability
Given a post displays the language switch control When evaluated for accessibility and interaction Then text contrast >= 4.5:1 and icon contrast >= 3:1 against background; focus indicator contrast >= 3:1 And the control is operable via keyboard (Tab/Enter/Space) and screen readers; it has a localized, descriptive accessible name including the current language And activating the control updates the post language, persists the per-post override, updates lang/dir attributes, and announces the change via aria-live polite within 500 ms And no WCAG 2.1 AA violations are reported for the control in automated checks
Screen Reader Language Tags for Mixed-Language Content
Given a translated post that includes mixed-language segments (translated body, quoted original text, usernames/hashtags) When inspected via the accessibility tree and tested with NVDA (Windows), VoiceOver (iOS), and TalkBack (Android) Then each segment has the correct lang attribute; no conflicting top-level lang overrides the segment-specific tags And screen readers pronounce each segment with the appropriate TTS voice And automated accessibility checks report 0 critical violations related to language or direction attributes
Notification Language Consistency with In-App Views
Given the detected language for a post is L and a user override O may be set When a push, email, or SMS notification is sent for that post Then the notification body language matches the in-app post language at send time (O if set, else L) And if the override changes before send, queued notifications use the latest selection And if the device language changes, subsequent notifications adapt after the next detection cycle; already-sent notifications remain unchanged And test runs across 5 locales confirm matching content hashes or string ids between notification and in-app render
Analytics and Audit Trail
"As a board admin, I want visibility and audit logs of which language each message was delivered in so that I can improve engagement and prove compliance."
Description

Capture and report delivery language, override rates, and read rates by language at post and community levels to assess effectiveness. Provide per-member audit trails that show the final delivery language and the decision path (policy, device locale, behavior model) for compliance review. Offer CSV export and privacy-safe aggregation, with configurable retention. Surface insights to admins (e.g., top languages, posts with high override rates) to refine policies and improve engagement.

Acceptance Criteria
Post-Level Language Delivery Metrics
- Given a post is published and delivered in multiple languages, When an admin opens the post analytics for that post, Then the view displays for each language: deliveries, unique readers, read rate (%), overrides, and override rate (%) and totals for the post. - Given authoritative event logs, When analytics are computed, Then per-language counts and rates match the underlying events within 0 variance for the same time window. - Given the admin selects a time filter (Last 7/30/90 days or custom range), When applied, Then all metrics recalculate to that window. - Given a post with up to 10,000 recipients, When analytics load, Then the dashboard renders in ≤2 seconds on a median broadband connection. - Given privacy constraints, When any language bucket has <10 deliveries in the selected window, Then counts are masked and rates are suppressed for that bucket.
Community-Level Language Analytics
- Given a community with multiple posts over time, When an admin views community analytics, Then the dashboard shows top N languages by deliveries, read rate by language, and override rate by language for the selected time window. - Given the admin changes the aggregation (post, week, month, quarter), When applied, Then charts/tables update accordingly with consistent totals. - Given privacy rules, When any bucket has <10 deliveries, Then bucket counts are masked and excluded from rate calculations; totals exclude masked buckets and indicate masking occurred. - Given authoritative logs, When aggregations are recomputed nightly, Then daily rollups match the sum of post-level metrics for the same period. - Given a community with >100k deliveries in the window, When analytics load, Then first contentful paint occurs in ≤3 seconds and complete data within ≤6 seconds.
Per-Member Delivery Language Audit Trail
- Given an admin with Compliance Auditor permissions, When they open a member’s audit for a specific post, Then the audit shows the final delivery language and the decision path (policy, device locale, behavior model with confidence, manual override) with timestamps. - Given an admin without required permissions, When they attempt to access a member audit, Then access is denied and no PII is exposed. - Given a delivered notification, When the delivery language is determined, Then the system records an immutable audit entry with event ID, inputs used, and decision outcome. - Given a member toggles a per-post language override, When the override is saved, Then the audit trail records actor, timestamp, old→new language, and the effective delivery language for that post only. - Given retention policy is active, When an audit record exceeds its retention period, Then detailed audit entries are purged and the UI/API returns an "expired by retention" indicator for that record.
Override Event Capture and Analytics
- Given a member taps the one-tap language override on a post, When they select a different language, Then an override event is stored and the post-level override rate increments for that language and decrements for the prior language if applicable. - Given a member toggles override multiple times on the same post, When analytics are computed, Then only the last saved selection within the window is counted toward override rate. - Given admins view post analytics, When the page loads, Then languages with override rates ≥10% are highlighted and sortable. - Given privacy rules, When a language has <10 recipients, Then override rates for that language are suppressed and marked as masked.
Analytics and Audit Trail CSV Export
- Given an admin requests export, When they choose Export Type (Post-Level Analytics, Community-Level Analytics, Member Audit Trail) and a time range, Then a CSV is generated with columns appropriate to the type. - Given Post-Level Analytics export, When generated, Then each row contains: post_id, post_title, community_id, language_code (ISO 639-1), deliveries, unique_readers, read_rate, overrides, override_rate, window_start_utc, window_end_utc, generated_at_utc. - Given Member Audit Trail export, When generated, Then each row contains: member_id, post_id, final_language, decision_path (ordered), timestamp_utc, actor, confidence_score (if model used), and excludes masked records per retention/privacy policies. - Given privacy masking, When a bucket is masked in the UI, Then the corresponding CSV shows masked values as "<10" counts and blanks rates. - Given datasets up to 1M rows, When export is requested, Then the file is available within 5 minutes and the download link expires after 24 hours or after one successful download, whichever comes first.
Privacy-Safe Aggregation and Data Retention
- Given a configured k-anonymity threshold of 10, When aggregating by language, Then any group with count <10 is masked and excluded from rate calculations. - Given an admin sets retention (Audit Detail: 90–365 days; Aggregated Analytics: 1–3 years), When saved, Then the setting is persisted, versioned, and auditable. - Given retention windows elapse, When nightly purge runs, Then detailed audit records older than the threshold are deleted and rollups are retained; purge actions are logged with counts removed. - Given legal hold is enabled, When applied to a member or post, Then purge skips matching records until the hold is cleared. - Given API requests for purged records, When queried, Then the API returns 410 Gone with reason "expired_by_retention".
Admin Insights: Top Languages and High Overrides
- Given community analytics over the last 30 days, When the insights module runs, Then it surfaces: top 5 languages by deliveries, languages with lowest read rate, and posts with override rate ≥ configurable threshold (default 10%). - Given an insight card is clicked, When the admin opens it, Then they can drill down to the underlying post or filtered analytics view. - Given thresholds are configured, When updated, Then subsequent insight runs use the new thresholds and display the effective values. - Given data refresh cadence is hourly, When an hour has passed, Then insights reflect new data with a refreshed timestamp. - Given an export request from insights, When executed, Then a CSV containing the insight results and parameters is generated with the same privacy masking rules.

Layout Lock

Preserves original formatting, images, tables, and invoice line items pixel‑for‑pixel in translation, so bills, dates, and amounts remain unambiguous and trustworthy.

Requirements

Pixel-Perfect Translation Renderer
"As a community manager, I want translated invoices to look exactly like the originals so that homeowners trust amounts and dates and I avoid disputes."
Description

Implement a deterministic rendering pipeline that replaces translatable text while preserving original coordinates, bounding boxes, fonts, line heights, images, and vector elements, producing a pixel-for-pixel match of the source layout across web and PDF outputs. Support ingestion of HTML posts and uploaded PDFs used for Duesly announcements and invoices, maintain the original z-order and spacing, and embed font subsets or apply metric-compatible emulation where licensing restricts embedding. Ensure identical pagination, margins, and footer/header positions so amounts, dates, and identifiers remain visually unchanged. Provide APIs to request a locked render for billing posts and return an immutable artifact for distribution via the Duesly feed and email.

Acceptance Criteria
Web Pixel Parity for Translated HTML Announcement
Given an HTML announcement containing images, tables, and styled text When a locked render is requested for web Then every text run retains original line breaks, letter spacing, and alignment And each element’s bounding box (x, y, width, height) differs by no more than 1px from the source And original z-order is preserved with no overlap changes And images and vector elements render at identical positions and sizes And computed fonts are either the embedded original or a metric-compatible fallback with unchanged line wrapping
PDF Pixel Parity for Translated Invoice
Given an uploaded invoice PDF with line items, headers, footers, and page numbers When a locked render is requested for PDF Then pagination (page count and page breaks) is identical to the source And page margins and header/footer positions are identical within 1pt And all amounts, dates, and identifiers remain at the same coordinates within 1pt And table column boundaries and line item row heights are unchanged And vector graphics remain vectors (no rasterization of vector-only pages)
Deterministic Rendering Output
Given the same source document, translation map, and rendering settings When the locked render is produced multiple times Then the byte-level hash of the generated artifact is identical across runs And the artifact’s content hash is returned in the API response for verification And any change to inputs produces a different content hash
Font Embedding and Metric-Compatible Emulation
Given a source document referencing licensed fonts When embedding is permitted Then the renderer embeds only the used font subsets And the resulting PDF passes preflight for embedded subset fonts When embedding is restricted Then metric-compatible fonts are substituted And line breaks, glyph advances, and baseline metrics produce unchanged wrapping and alignment And numerals used in amounts and dates retain monospaced alignment where present
Coordinate and Bounding Box Preservation for Replaced Text
Given any translatable text run replaced by translation When rendered with Layout Lock Then the new text occupies the original run’s bounding box and anchor coordinates within 1px (web) and 1pt (PDF) And no layout reflow affects neighboring runs, images, or tables And hit targets and annotation anchors (e.g., links) remain aligned to their original areas
Z-Order and Transparency Preservation
Given overlapping text, images, and vector layers with transparency When rendered with Layout Lock Then stacking order matches the source And blend modes and alpha are preserved And no element that was fully visible becomes occluded, and no previously occluded element becomes visible
Locked Render API and Immutable Distribution Artifact
Given a billing post or announcement ID and translation map When the client calls the Locked Render API Then the service returns a unique immutable artifact ID, content hash, and URLs for web and PDF And the artifact is write-once; subsequent modifications create a new artifact ID And the artifact is distributable via the Duesly feed and email without further transformation And the API responds within 2 seconds for HTML posts and 5 seconds for PDFs up to 10 pages at p95
Table and Line-Item Structure Preservation
"As a treasurer, I want line items and totals to stay aligned after translation so that charges remain clear and auditable."
Description

Preserve invoice table geometry by locking column widths, row heights, and cell padding while translating only label and description fields. Enforce numeric field invariants by excluding amounts, totals, and dates from translation and maintaining right alignment, decimal precision, and currency symbols. Use tabular numerals or font features to keep columns aligned, and prevent wrapping that could misplace totals or footnotes. Integrate with Duesly’s bill model to map each cell to a schema field, enabling safe translation of descriptors while protecting computed values and summaries.

Acceptance Criteria
Geometry Locked During Translation
Given an invoice with a line‑item table with defined column widths, row heights, and cell padding And label/description columns marked as translatable When the invoice is translated into a target language with 30–50% text expansion Then the computed column widths, row heights, and cell padding values equal the pre‑translation values And the table’s outer width and height equal the pre‑translation values And no additional rows or columns are created, removed, or reflowed as a result of translation
Numeric Fields Protection and Alignment
Given amount, quantity, tax, subtotal, total, and date cells are flagged non‑translatable and read‑only When translation is executed Then the text content of these cells remains byte‑for‑byte identical to the source And numeric cells are right‑aligned both before and after translation And decimal precision (number of fraction digits) is unchanged for every numeric cell And currency symbols and their positions relative to numbers are preserved And thousand/decimal separators and negative sign placement are unchanged from the source
Tabular Numerals Maintain Column Alignment
Given numeric columns use a font that supports OpenType tabular numerals (tnum) or a defined fallback When rendering numeric cells (quantities, prices, totals) Then the tabular numerals feature is enabled for those cells or a tabular‑width fallback font is applied And digits 0–9 render with equal advance width across the column And decimal separators align vertically across rows when right‑aligned And there is no horizontal jitter or misalignment between rows for the same digit counts
No Wrap; Totals and Footnotes Stay Anchored
Given translated label/description text exceeds the original column width When the invoice table renders post‑translation Then label/description cells do not wrap to additional lines (single‑line rendering only) And any overflow is handled without increasing row height (e.g., clip/ellipsis) so row heights equal pre‑translation values And the totals/summary row remains the last row and is not displaced And footnote markers and footnote rows remain anchored to their original line items
Cell‑to‑Schema Mapping for Safe Translation
Given each table cell is mapped to a Duesly bill model field (e.g., item.name, item.description, header.label, item.amount) When building the translation payload Then only fields designated as translatable (labels and descriptions) are included And non‑translatable/computed fields (amounts, taxes, subtotals, totals, dates) are excluded and remain read‑only And if any cell lacks a schema mapping, the translation is aborted with an error and no document changes are committed And an audit log entry records the field keys translated and the mapping version used
Date Fields Format and Content Preserved
Given date cells (e.g., Invoice Date, Due Date) are flagged as non‑translatable When translation is performed Then date cell strings are identical to the source values And their display formats (e.g., YYYY‑MM‑DD or MM/DD/YYYY) remain unchanged And alignment and padding for date cells remain unchanged And timezone/offset metadata associated with date fields remains unchanged
Decimal Precision and Totals Integrity
Given line items, taxes, discounts, and totals are computed values When translation occurs Then the number of displayed fraction digits per numeric cell matches the pre‑translation state And the post‑translation totals equal the pre‑translation totals exactly (no rounding drift) And checksum validation of numeric fields (e.g., sum of line amounts + tax − discounts = total) passes pre‑ and post‑translation And no locale‑specific numeric reformatting is applied to computed fields
Text Expansion Control and Font Fallback
"As a board member, I want translated text to fit within the original layout so that nothing shifts or overlaps."
Description

Introduce language-aware strategies to keep translated strings within fixed boxes without layout drift. Apply prioritized techniques including micro letter-spacing adjustments, hyphenation, soft-wrapping at safe breakpoints, condensed font variants, and metric-compatible fallback stacks for missing glyphs. Provide per-field maximums, truncation with tooltip disclosure where allowed, and glossary-based abbreviations for common phrases. Maintain consistent baselines and line heights, and ensure these adjustments are deterministic to keep renders stable across devices. Expose settings per template to balance fidelity and readability for different languages used in Duesly communities.

Acceptance Criteria
German Line-Item Description Fits Within Fixed Width Without Layout Drift
Given a template with a fixed-width line-item Description field and language set to German And per-field width maximum equals the container width And truncation is not allowed for this field When the translated Description initially exceeds the container width Then techniques are applied in this priority order until the text fits: (1) letter-spacing adjustment within -2% to 0% of em, (2) language hyphenation, (3) soft-wrapping at safe breakpoints, (4) condensed font variant up to the configured minimum width (no smaller than 90% of normal) And the final rendered text bounding box width is <= container width with no overflow or overlap And neighboring elements’ positions (top/left) remain unchanged versus the English layout (no layout drift) And line height remains unchanged (±0.5 px tolerance) And the same line breaks occur across Chrome, Firefox, Safari, iOS, Android, and generated PDF for the same viewport width
Hyphenation and Safe Soft-Wrap for Compound Words in Labels
Given a label string in a language that supports hyphenation And the unbroken string would overflow its container by > 0% and <= 8% When hyphenation is enabled for that language Then hyphenation occurs only at dictionary-approved points and inserts a visible hyphen per language rules And no hyphenation or soft-wrap occurs within numbers, currency amounts, dates, or between a currency symbol and its amount And after hyphenation/soft-wrap, the string fits the container (no overflow) without changing font size And the resulting breakpoints are deterministic and identical across supported browsers and PDF output for an identical container width
Condensed Font Variant Activation Near Overflow Threshold
Given a field where letter-spacing and hyphenation/soft-wrap have been applied And the text still exceeds the container width by <= 10% And a condensed or semi-condensed variant is available in the font family When rendering the field Then the system switches to progressively more condensed variants in 5% width steps down to the template’s configured minimum (no less than 90% of normal) And baseline and line height remain unchanged (±0.5 px tolerance) And glyph shapes for digits and currency symbols remain from the same family to preserve tabular alignment And after the switch, the text fits without overflow and without causing sibling element repositioning
Metric-Compatible Font Fallback Preserves Line Breaks for Missing Glyphs
Given a translated string that includes glyphs missing from the primary font (e.g., CJK, accented characters, emoji) And a template-configured fallback stack of metric-compatible fonts exists When the field is rendered Then missing glyphs are rendered using the next available font in the fallback stack And the line height and advance width metrics for fallback glyph runs differ by <= 1% from the primary font’s metrics for layout purposes And the number and positions of line breaks match those produced with ideal full-coverage metrics (golden snapshot), across browsers and PDF And no clipping, overlapping, or reflow of adjacent elements occurs
Per-Field Maximums with Truncation and Tooltip Disclosure (Optional Fields)
Given a field marked as “Truncation Allowed” with a max of 2 lines and container width constraints And after applying letter-spacing, hyphenation, soft-wrap, and condensed variants the text still does not fit When rendering the field Then the text is truncated at a whole grapheme cluster boundary, an ellipsis is appended, and the visible content respects the 2-line limit without overflow And a tooltip/tap-to-expand discloses the full untruncated text on hover/focus/tap, localized to the user’s language And critical fields (amounts, dates, invoice numbers) are never truncated regardless of settings And copy-to-clipboard returns the full underlying text, not the truncated display
Baseline and Line-Height Consistency Across Languages and Devices
Given a template with a defined baseline grid and line-height values When rendering the same invoice in English, German, Japanese, and Spanish across Chrome, Firefox, Safari, iOS, Android, and exported PDF Then the first text baseline of each field aligns to the same grid line across languages (deviation <= 0.5 px) And per-field line height in pixels remains constant across languages and platforms (±0.5 px) And toggling between languages does not cause vertical reflow beyond the text box boundaries And screenshot diff for the same language across platforms shows <= 0.5% pixel variance for text layout regions
Template-Level Language Controls and Deterministic Rendering
Given a template editor with per-language settings (priority order of techniques, letter-spacing min/max, hyphenation toggle, soft-wrap rules, condensed variant minimum, fallback stack order, per-field truncation/tooltip settings, glossary abbreviations) When an admin updates these settings and saves the template Then the settings are persisted versioned-per-template and apply only to that template And preview renders reflect the changes immediately for selected languages And identical inputs (text, container width, settings) produce identical line breaks, letter-spacing, and font selections across browsers and PDF (hash of layout metrics stable) And non-admin users cannot modify these settings (permission enforced) And a glossary abbreviation is applied only where configured, never within amounts, dates, or legal terms
Bi-directional and Complex Script Support
"As an owner who reads Arabic, I want Arabic text displayed correctly within the original invoice layout so that the document remains readable and trustworthy."
Description

Support correct shaping and ordering for RTL languages and complex scripts within fixed containers while preserving the original left/right positional layout. Handle mixed LTR numerals inside RTL text for amounts and dates, ligatures, diacritics, and line breaking rules for Arabic, Hebrew, and CJK. Use a shaping engine or browser-native shaping with consistent font metrics to avoid reflow. Provide per-field directionality metadata and automatic detection, ensuring the overall document structure remains unchanged while text renders correctly for the target audience.

Acceptance Criteria
RTL Invoice Header with Mixed LTR Numerals
Given a fixed-position invoice header containing Arabic RTL text with embedded LTR numerals for invoice number and date When rendered in Chrome, Firefox, Safari and exported to PDF Then Arabic text shapes correctly with ligatures and diacritics And embedded numerals retain left-to-right order within the RTL run And all text baselines and x/y positions match the source within +/- 1 px And no overlap, clipping, or reflow occurs across outputs And the visual output matches the golden snapshot with <= 1% pixel difference And the accessibility reading order follows the intended visual order using bidi isolates
Arabic Line Item Table Preserves Cell Layout
Given a fixed-width line-item table with Arabic descriptions, quantities (LTR numerals), and prices When text is shaped and wrapped inside each cell Then column widths, cell positions, and row heights remain within +/- 1 px of the source And line breaks occur only at valid Arabic break opportunities (no breaks at prohibited characters) And LTR numerals display left-to-right while aligning correctly in the RTL context And decimal separators and currency symbols are not separated from numerals And totals column remains vertically aligned with no reflow in PDF export
Hebrew Address Block in Left-Fixed Container
Given a left-fixed container holding Hebrew address lines with embedded Latin apartment codes and phone numbers When the content is rendered Then Hebrew flows right-to-left and Latin substrings remain left-to-right with correct neutral punctuation placement And the container’s bounding box and text baselines match the source within +/- 1 px And there is no text overflow, clipping, or unintended truncation And any font fallback maintains line metrics within 2% without causing reflow And the PDF output matches the on-screen layout within <= 1% pixel difference
CJK Line Breaking for Dates and Amounts
Given a fixed container with Chinese item descriptions, dates, and amounts When content is rendered and wrapped Then kinsoku line breaking rules are respected (no break before closing punctuation or after opening punctuation) And currency symbols and numerals are not separated across lines And no hyphenation is introduced And glyph positions and line breaks match the source within +/- 1 px across browsers and PDF And there is no change to table or image positions adjacent to the text
Per-field Directionality Metadata Overrides Autodetect
Given fields configured with direction metadata (rtl, ltr, auto) When users input mixed-script text Then the base direction follows the field’s metadata; if auto, base direction is derived per Unicode bidi rules for that field And the resolved direction is indicated in the UI and stored with the field And toggling metadata updates visual order without moving the field’s container or affecting siblings And reloading the document and exporting to PDF preserves the resolved direction and layout within +/- 1 px
No Reflow Between Browser and Shaping Engine
Given a document rendered with browser-native shaping and exported via a server-side shaping engine When comparing on-screen and exported outputs Then line breaks, glyph shaping, and positions are identical within +/- 1 px And container heights differ by <= 2% with no additional pages introduced And tables, images, and invoice line items retain their original positions And no glyph substitution or missing glyph boxes occur And visual diff between outputs remains <= 1% pixel difference
Embedded Image Text Overlay
"As a property manager, I want text inside images translated without altering the underlying image so that the document remains authentic yet readable."
Description

Detect text embedded in images (stamps, scanned headers, seals) via OCR and offer a non-destructive translation overlay that preserves the underlying pixels. Render translated text on a separate layer using matched fonts or stylings, aligned to the original positions, with opacity controls to avoid obscuring critical imagery. Allow toggling overlays in the Duesly viewer and include alt text for accessibility and search without altering the source artifact. Ensure the exported PDF and feed previews retain authenticity while providing clarity for non-source-language readers.

Acceptance Criteria
OCR Detection of Embedded Image Text
Given a page containing embedded text in images (stamps, scanned headers, seals) When OCR is executed at 300 DPI on an A4 page Then ≥95% of text regions with x-height ≥10 px and contrast ratio ≥4.5:1 are detected and boxed with coordinates in page space And each box includes a confidence score per span; spans with confidence <0.80 are flagged as low-confidence And total OCR processing time is ≤1.5 seconds per page on a 2 vCPU / 4 GB RAM worker And no source pixels are altered; only metadata and overlay artifacts are produced And OCR errors and performance metrics are logged with correlation IDs
Overlay Font Matching and Alignment
Given recognized text spans with font size, style, and baseline geometry When the translated overlay is rendered Then overlay text aligns to the original span's bounding box with ≤2 px positional error at 100% zoom and scales proportionally at all zoom levels And font family/weight/style are matched or a best-fit fallback is chosen to keep line width within ±5% of the original; for unsupported scripts, a script-appropriate fallback is used And line breaks and paragraph flow mirror the source regions and do not wrap outside the original region bounds And overlays render on a separate layer above the image layer without modifying underlying pixels
Viewer Overlay Controls (Toggle and Opacity)
Given a user viewing a document with detected overlays When the user toggles "Show translation overlay" Then the overlay layer visibility switches on/off without page reflow or image shifts And the toggle state persists per user per document for 30 days And an opacity control from 0% to 100% in 5% increments updates live and is keyboard-accessible (WCAG 2.2 AA) And default overlay opacity is 70% and can be reset to default in one action And zoom (25%–400%) and pan do not desynchronize overlay positioning
Non-Destructive Export and Feed Preview
Given a document with translation overlays When the user exports to PDF or views a feed preview Then the source image/PDF content remains unaltered (no raster burn-in of translated text) And overlays are embedded as a separate Optional Content Group (OCG) layer labeled "Translation Overlay" that is visible by default and toggleable in compliant PDF viewers And export preserves original file resolution and increases file size by ≤20% due to overlays And feed previews display the overlay visually while retaining underlying source pixels intact
Accessibility and Search Indexing
Given overlays exist on a page When assistive technologies are used Then each overlay span exposes alt text with appropriate lang attributes for the target language and references the source text And reading order follows the source layout; overlays are reachable and operable via keyboard And document search returns matches from overlay text with hit-highlighting that maps to overlay and source regions And disabling overlays does not remove searchability or screen-reader access to overlay text
Numeric and Date Fidelity in Translation
Given detected text contains numbers, currency, or dates When translation is applied to create the overlay Then numeric values, currency symbols/codes, and decimal precision are preserved exactly; thousand/decimal separators are localized without altering numeric magnitude And date values are reformatted to the viewer's locale while preserving the original ISO value as metadata And amounts in overlays are selectable/copyable as normalized text And a checksum comparator flags any post-translation numeric mismatch and suppresses the overlay for that span with a tooltip indicating "source-only (numeric fidelity)"
Layout Integrity Validator and Diff
"As a compliance officer, I want an automated check that flags layout drift and numeric changes so that I can safely approve translations."
Description

Provide an automated validation step that compares pre- and post-translation renders at pixel and structural levels. Flag overflow, clipped text, reflow beyond thresholds, unintended font fallbacks, and any changes to protected numeric fields, dates, or IDs. Generate a side-by-side visual diff and heatmap, produce a pass/fail status, and block publication to the Duesly feed if validation fails. Log results and artifacts to the audit trail and expose them in an approval workflow for managers before bills are sent or posted.

Acceptance Criteria
Pixel-Level Diff and Heatmap Generation
Given a pre-translation render and a post-translation render of the same document version at 300 DPI When the Layout Integrity Validator runs Then it produces for each page a side-by-side visual diff and a pixel-level heatmap within 5 seconds for documents up to 10 pages And the heatmap resolution matches the render resolution and marks all pixels where any RGBA channel differs from pre-render And it computes and stores per-page and whole-document pixel-difference percentages And it attaches links to the diff images and heatmaps to the validation result
Structural Reflow and Overflow Detection
Given template thresholds: max text baseline shift ≤ 2 px, max element bounding-box shift ≤ 2 px, max line-wrap delta ≤ 1 wrap per paragraph When comparing pre- and post-translation layouts Then the validator flags any clipped or overflowed text, or any shift exceeding thresholds And it lists each violation with page number, element identifier, measured deltas, and region snapshots And the validation result is Fail if any violation is present
Protected Field Integrity Enforcement
Given protected fields (amounts, dates, invoice IDs, account numbers) are defined by selectors/anchors for the template When values are extracted from pre- and post-translation renders using the same extraction rules Then numeric values and IDs must match exactly after locale-normalization; date values must represent the same ISO date And any mismatch or parse error results in Fail and highlights the affected field regions in the diff And zero tolerance is applied (0 allowed difference)
Unintended Font Fallback Detection
Given a font whitelist and fallback rules are configured for the template When the post-translation render substitutes a font outside the whitelist for glyphs previously rendered with a whitelisted font Then the validator flags the substitution as a font fallback violation And allowed fallbacks scoped to specific code points do not trigger violations And the report includes page, text sample, original font, substituted font, and code point ranges
Publication Gate on Validation Outcome
Given a document is queued for posting to the Duesly feed or for sending When the latest validation status is Fail Then publication and sending are blocked in UI and API And attempts via API return HTTP 403 with reason VALIDATION_FAILED and a link to the validation report And when status is Pass, the gate allows publication without additional prompts
Audit Trail Logging of Validation Results and Artifacts
Given a validation run completes with Pass or Fail When results are written to the audit trail Then the log entry includes timestamp, actor, document ID and hash, validator version, thresholds used, outcome, and counts of violations And it stores immutable links to artifacts (diffs, heatmaps, element-level CSV/JSON) with retention ≥ 365 days And subsequent access to artifacts is RBAC-protected and logged with user, time, and action
Manager Approval Workflow Exposure
Given approval is required by policy for bills prior to posting When a manager opens the approval task for a validated document Then they can view the side-by-side diff, heatmap, protected fields check summary, and violation list within two clicks from the task And they can Approve, Reject, or Request Changes; Reject and Request Changes require a comment And Approve unblocks publication, while Reject or Request Changes keep it blocked and notify the submitter
Lock Workflow, Roles, and Audit
"As an admin, I want to lock and version translated documents with an audit trail so that billing uses verified, tamper-proof layouts."
Description

Introduce a lock/unlock workflow governed by role-based permissions so only authorized users can create, approve, and publish layout-locked artifacts. Capture version history, timestamps, approvers, and checks performed by the validator, and generate immutable IDs referenced by billing and reminder systems. Prevent edits to content that would invalidate the lock and require re-validation on changes. Provide API/webhooks for downstream systems to fetch the locked artifact and confirm its integrity when sending announcements, invoices, and automated reminders.

Acceptance Criteria
RBAC Controls for Lock, Approve, and Publish
Given a draft artifact and a user without the required permission for the attempted action (lock, approve, publish, unlock), When the user invokes that action via UI or API, Then the system returns 403 Forbidden and writes an audit entry capturing user_id, action, timestamp, and reason "insufficient_permission". Given a draft artifact and a user with Validate and Lock permissions, When all validator checks pass and the user confirms lock, Then the artifact status transitions to "Locked". Given a Locked artifact and a user with Approve permission, When the user approves, Then the artifact status becomes "Approved". Given an Approved artifact and a user with Publish permission, When the user publishes, Then the artifact status becomes "Published" and the content digest remains unchanged. Given any artifact and a user with Unlock permission, When the user unlocks providing a valid reason, Then a new draft version is created and the prior locked version remains immutable.
Validator Checklist Gating Lock
Given a draft artifact ready for lock, When the validator runs, Then the system evaluates all mandatory checks (layout preservation, totals consistency, date formats, currency, required fields present) and displays pass/fail per check with check_ids. Given any mandatory check fails, When a user attempts to lock, Then the system blocks the lock with 409 Conflict and lists failing check_ids. Given all mandatory checks pass and no warnings exceed the configured severity threshold, When a user with Lock permission locks the artifact, Then the system permits lock and stores the checklist results with timestamps and actor_id. Given validator configuration changes after lock, When no content changed, Then the existing locked artifact remains valid and is not auto-invalidated.
Locked Content Immutability and Re-validation on Change
Given a Locked or Approved artifact, When a user attempts to edit protected fields (layout, images, tables, invoice line items, amounts, dates, currency, tax rates, totals), Then the system prevents the edit with 409 Conflict and identifies the protected field. Given a user initiates Unlock on a locked artifact, When they provide a reason of at least 10 characters and have Unlock permission, Then the system creates version N+1 in Draft status, revokes prior approvals, and flags Re-validation required. Given version N+1 is modified, When the validator passes, Then the artifact can be re-locked and receives a new immutable ID and digest.
Comprehensive Audit Trail and Version History
Given any state transition (lock, unlock, approve, publish), When the action completes, Then the system appends an immutable audit record capturing artifact_id, version, actor_id, action, timestamp (UTC), prior_status, new_status, and request origin (UI/API/IP). Given a validator run occurs, When results are produced, Then the system stores the checklist results (check_id, name, pass/fail, details, duration) linked to the version. Given an admin or integrator requests audit for an artifact via UI or API, When the artifact exists, Then the system returns a complete chronological history with a cryptographic hash per entry to detect tampering. Given an attempt is made to alter or delete an audit record, When processed, Then the system rejects the operation and logs a security event.
Immutable Artifact ID and Content Digest Generation
Given a draft artifact that passes validation, When it is locked, Then the system generates an immutable globally unique artifact_id (ULID or UUIDv4) and a SHA-256 digest of the normalized locked content bytes. Given any content change after an unlock, When the artifact is re-locked, Then a new artifact_id and digest are generated and the previous ID remains permanently bound to its original content. Given a billing or reminder job is created referencing a locked artifact, When the job is saved, Then artifact_id and digest are required fields and must match the stored values. Given an attempt is made to associate an existing artifact_id with content whose digest differs, When validated, Then the system rejects with 409 Conflict.
Downstream API and Webhook Distribution
Given a locked, approved, or published artifact, When a client calls GET /api/locked-artifacts/{artifact_id}, Then the system returns 200 with metadata (artifact_id, version, status, timestamps, digest, approver_id) and the canonical content whose bytes hash to the digest; ETag equals the digest. Given a subscriber has configured a webhook endpoint, When events artifact.locked, artifact.approved, or artifact.published occur, Then the system delivers a signed POST (HMAC-SHA256) containing artifact_id, digest, version, status, actor_id, timestamps, and retries with exponential backoff for at least 24 hours upon non-2xx. Given a client calls HEAD on the artifact resource, When the artifact exists, Then the system returns 200 with ETag set to the digest to enable integrity checks without downloading content.
Integrity Verification When Sending Billing and Reminders
Given an announcement, invoice, or reminder references an artifact_id, When sending is triggered, Then the system verifies the artifact exists, has status in ["Approved","Published"], and that the provided digest equals the stored digest. Given verification fails, When sending, Then the system aborts delivery, records an error with cause (missing_artifact, wrong_status, digest_mismatch), and notifies the initiator. Given verification succeeds, When sending, Then the system includes artifact_id and digest in the outbound payload or headers, attaches canonical content if applicable (byte-for-byte matching the digest), and writes a success audit entry.

Glossary Guard

Admin-defined glossary locks HOA names, legal phrases, fees, and acronyms to consistent terms or keeps them untranslated. Prevents costly misinterpretations and speeds approvals.

Requirements

Glossary Admin Console & RBAC
"As a board admin, I want a single place to define and govern official terms and phrases so that our community’s communications stay consistent and legally correct."
Description

Provide a centralized admin console where authorized users define and manage canonical terms, protected phrases, acronyms, and forbidden variants for each community (and global defaults). Support fields for canonical term, allowed synonyms, disallowed forms, capitalization rules, pluralization, locale-specific variants, effective dates, and definitions for hover-tooltips. Include term categories (legal, fees, property assets), notes, and deprecation flags. Enforce role-based access: only board admins or managers with the Glossary permission can create, edit, or delete entries; editors can view and request changes. Integrates with Duesly’s tenant model to allow community-level overrides of global policies and with notification settings to alert stakeholders of glossary updates.

Acceptance Criteria
RBAC Enforcement for Glossary Admin Console
Given a user with role Board Admin or Manager and the "Glossary" permission When they access the Glossary Admin Console or its APIs for a community they belong to Then they can create, edit, and delete glossary entries scoped to that community Given a user with role Editor without the "Glossary" permission When they access the Glossary Admin Console Then they can view entries in read-only mode and submit change requests but cannot save edits or delete entries Given a user without access to the target community When they request glossary resources via UI or API Then the system returns 403 Forbidden and does not reveal resource existence Given any write action is attempted by an unauthorized user When the request is processed Then the action is blocked and no data is modified
Term Creation Fields & Validation
Given a permitted admin is creating or editing a glossary entry When they open the term form Then the form provides fields: canonicalTerm (required, <=100 chars, unique per community+locale), allowedSynonyms (0–20 items, each <=100 chars), disallowedForms (0–50 items, each <=100 chars), capitalizationRule (TitleCase|Uppercase|Lowercase|AsEntered), pluralizationRule (Auto|Custom|None; if Custom then customPlural required, <=100 chars), localeVariants (0–20 localeCodes with variant term), effectiveStart (ISO-8601, optional) and effectiveEnd (ISO-8601, optional, >= start if set), definition (0–500 chars), category (required: Legal|Fees|PropertyAssets), notes (0–1000 chars), deprecated (boolean) Given the admin enters values When they attempt to save Then validation enforces: canonicalTerm not in disallowedForms; allowedSynonyms do not overlap disallowedForms; no duplicate synonyms within the entry; (canonicalTerm, locale) is unique across entries in the same scope (global or community); effectiveEnd is not before effectiveStart Given any validation rule is violated When Save is clicked Then the save is blocked and field-level error messages are displayed Given all validation rules pass When Save is clicked Then the entry is persisted and appears in the glossary list for that scope
Community-Level Overrides of Global Glossary
Given a global default glossary entry exists When a community admin creates an override with the same canonical term for their community Then the override is marked as community-scoped, takes precedence in that community, and the global entry remains unchanged Given a community override exists When the override is deleted by a permitted admin Then the community reverts to using the global entry Given multiple communities create overrides for the same global term When terms are resolved per community Then each community sees its own override without affecting others Given a community admin creates a new term without a global counterpart When the term is saved Then it is visible only within that community and not to others
Stakeholder Notifications on Glossary Changes
Given community notification settings are configured When a glossary entry is created, updated, deprecated, deleted, or overridden Then a GlossaryUpdated event is emitted with communityId, entryId, action, actorId, timestamp, and change summary Given recipients are configured to receive Glossary updates When the event is emitted Then notifications are sent via configured channels per recipient preferences (e.g., immediate or batched) and opt-outs are respected Given a notification is dispatched When delivery is attempted Then success or failure is recorded and visible in the community's notification activity
Effective Date Scheduling and State Transitions
Given a glossary entry has effectiveStart in the future and no effectiveEnd When the glossary list is filtered by Active Now Then the entry is labeled Scheduled and excluded from Active results until effectiveStart Given the current time is between effectiveStart and effectiveEnd (or no end) When term resolution occurs for that scope Then the entry is considered Active and is returned for matching Given the current time is after effectiveEnd When term resolution occurs Then the entry is considered Expired and is not returned for matching Given effectiveEnd is earlier than effectiveStart When saving the entry Then the save is blocked with a date validation error
Tooltip Definition Delivery and Rendering
Given a glossary entry includes definition text, category, and locale-specific variants When UI content renders a matching canonical term or allowed synonym in a supported locale Then a tooltip is available on hover/tap showing canonical term, localized definition (<=500 chars), and category, respecting the entry's capitalization rule Given a glossary entry has no definition When the term appears in UI content Then no tooltip is shown Given a glossary entry is marked deprecated When the tooltip is shown Then the tooltip includes a Deprecated indicator
Editor Change Request Workflow
Given a user with Editor role (no write permissions) views a glossary entry When they submit a change request including proposed edits and rationale Then a change request record is created as Pending and linked to the target entry and community Given a change request is Pending When a Board Admin or Manager reviews it Then they can Approve (applying the proposed changes to the entry) or Reject (leaving the entry unchanged) and must provide an optional comment Given a change request is approved or rejected When the decision is recorded Then the requester is notified per notification settings and the request status updates accordingly
Real-time Term Detection & Autocorrect in Composer
"As a content editor, I want instant, in-line guidance and one-click fixes for non-standard terms so that I can publish accurate posts without manual review cycles."
Description

Embed client-side detectors in all Duesly editors (announcements, invoice line items, compliance notices, comments) to highlight non-compliant terms as the user types or pastes. Provide inline suggestions to replace with the canonical term, one-click autocorrect, and optional hard-lock mode that prevents editing of protected phrases. Handle case sensitivity, punctuation, plural/possessive forms, and common OCR/paste artifacts. Show hover definitions from the glossary to reduce confusion. Ensure accessibility (keyboard actions, ARIA) and low-latency performance (<50ms per keystroke) across web and mobile.

Acceptance Criteria
Real-time Highlight and Suggest in All Editors
Given a user is composing text in any Duesly editor (Announcements, Invoice Line Items, Compliance Notices, Comments) When the input contains a term that does not match the glossary’s canonical or protected term Then the non-compliant term is visually highlighted within 50ms p95 of the triggering keystroke on reference web and mobile devices And an inline suggestion presents the canonical term adjacent to the highlight And the suggestion is fully operable via keyboard (focusable, Enter to apply, Esc to dismiss) and announced via ARIA for screen readers And if the term is marked Do Not Translate, no replacement action is offered and a lock badge explains it will remain untranslated And detection remains accurate across word boundaries, line breaks, and punctuation
One-click Autocorrect Preserves Context
Given a highlighted non-compliant term with an available canonical replacement When the user invokes Replace via click or keyboard shortcut Then the term is replaced with the canonical term while preserving surrounding punctuation, spacing, and capitalization style (lower/UPPER/Title) And plural and possessive forms are corrected to the canonical equivalent (including HOA/HOAs/HOA’s) And the caret position and selection are preserved intuitively after replacement And an audit log entry is recorded with before/after text, term ID, user, timestamp, and editor context And bulk replace applies to all highlighted instances within the current selection when chosen
Hard-lock Mode Blocks Edits to Protected Phrases
Given a glossary term flagged as Hard Lock appears in the editor When the user attempts to type, delete, or paste within the protected span Then the edit is blocked without corrupting surrounding text And an inline message and non-intrusive toast explain the lock with a link to the glossary definition And copy is allowed, but cut, delete, and overwrite are prevented And attempts via keyboard, mouse, mobile IME composition, or programmatic input are consistently prevented And only users with the Glossary Override permission can temporarily unlock, with all overrides audited
Robust Detection on Paste and OCR Artifacts
Given the user pastes content up to 10,000 characters that may include OCR artifacts (extra spaces, soft hyphens, smart quotes, broken lines) When the content is inserted Then the detector normalizes artifacts and highlights non-compliant terms in the pasted range And overlapping matches are resolved by longest-match-wins without duplicate highlights And inline suggestions are available for each highlight within 200ms p95 of paste completion on reference devices And no unintended whitespace or character changes occur until the user accepts a suggestion
Case, Plural, and Possessive Handling
Given glossary entries define canonical forms and case-sensitivity rules When the user types or pastes variants differing by case, pluralization, or possessives Then detections occur per rule (case-sensitive or insensitive as configured) And replacements preserve intended case pattern and apply correct plural or possessive morphology of the canonical term And adjacent punctuation (commas, periods, parentheses) does not prevent detection or produce incorrect replacements
Glossary Hover Definitions and Accessibility
Given a highlighted term in the editor When the user hovers with a mouse or focuses the highlight via keyboard Then a tooltip shows the glossary definition and metadata within 150ms and meets WCAG 2.1 AA contrast And the tooltip is announced via ARIA (role=tooltip, labelledby/-describedby), dismissible with Esc, and does not trap focus And on mobile, a long-press reveals an accessible action sheet with definition and Replace action
Mobile Stability and Performance
Given the user is typing in the mobile editor on supported iOS and Android devices When detections and replacements occur Then typing latency remains ≤ 50ms p95 per keystroke with no input drops during IME composition And the feature does not conflict with OS-level autocorrect (no double replacements or cursor jumps) And the detector runs offline using the last-synced glossary and indicates a disabled state if no glossary is available And glossary updates propagate to the editor within 2 seconds of change when online
Translation Lock & Phrase Preservation
"As a bilingual manager, I want protected terms to remain unchanged during translation so that legal meaning isn’t lost when communicating in multiple languages."
Description

Integrate Glossary Guard with Duesly’s localization pipeline to mark protected terms/phrases as non-translatable tokens, preserving exact strings and case across machine- or human-translated content. Support locale-aware canonical variants (e.g., leave legal phrases in English but localize fee labels when allowed). Provide pre- and post-translation checks to ensure tokens remain intact, with fallback to server-side correction if a translator alters protected text. Maintain an allowlist for acronyms that must remain unchanged in all locales.

Acceptance Criteria
Pre-Translation Tokenization and Tagging
Given source content contains one or more protected terms/phrases defined in Glossary Guard for the source locale And the localization pipeline is preparing the content for translation When pre-translation processing runs Then each protected term is replaced with a non-translatable token that preserves original text and case in token metadata And multi-word phrases are tokenized as a single unit without internal whitespace or punctuation changes And a token map is emitted with source offsets and canonical term IDs for all tokens And the job is marked failed with an error listing offending spans if any protected term cannot be tokenized
Machine Translation Preservation and Verification
Given content with protected tokens is sent to the configured machine translation engine for a target locale When the translation completes and detokenization is applied Then all tokens remain intact and restore the exact protected strings with original case And post-translation validation reports 0 token violations And only non-protected segments are translated And an audit entry records the validation outcome and counts
Human Translation Alteration Correction
Given a human translator edits localized content that includes protected tokens When post-translation validation detects a protected segment altered by the translator Then the system automatically reverts that segment to the protected source text with original case And records before/after values, translator identifier, timestamp, and affected token IDs in the audit log And notifies the submitter and project admin of the correction And a subsequent re-validation reports 0 token violations
Locale-Aware Canonical Variants Application
Given legal phrases are configured as "keep in English" and fee labels have locale-specific canonical variants When content is localized to a target locale Then legal phrases remain exactly as in the source language without translation And fee labels are rendered using their configured locale-specific canonical variants unless individually locked And case of canonical variants conforms to the configured style for the locale And a summary lists each canonical substitution applied with term ID and locale
Acronym Allowlist Enforcement Across Locales
Given an allowlist of acronyms is configured When content is localized to any target locale Then each allowlisted acronym appears exactly as configured, including punctuation and case And no transliteration, expansion, or diacritics are introduced And any deviation is auto-corrected and logged with the offending token and location
Error Handling and Server-Side Fallback on Provider Failure
Given the translation provider returns output where non-translatable tokens are missing, split, or malformed When post-translation validation runs Then the system reconstructs protected segments from the token map and source text And marks the job status as "Corrected" with details of each reconstruction And returns a warning in the job API response And the final output passes validation with 0 token violations
System-wide Enforcement Hooks (Feed, Billing, Compliance, Notifications)
"As an operations lead, I want glossary rules enforced everywhere content is created or sent so that no inconsistent terminology slips through any channel."
Description

Apply glossary enforcement both client-side and server-side across all content surfaces: feed posts, invoices/bills, compliance letters, reminders, emails, PDFs, and push notifications. Add middleware in content creation APIs and import endpoints to normalize terms and reject or auto-correct violations based on policy. Ensure rendered PDFs and email templates preserve protected phrases and use canonical terms. Provide settings to choose enforcement mode per surface (warn, auto-correct, block) and ensure idempotent processing on repeated sends.

Acceptance Criteria
Feed Composer Client-Side Enforcement (Warn/Auto-correct/Block)
Given a glossary with canonical mappings and locked phrases And Feed enforcement mode is Warn When a user types a non-canonical variant in the feed composer Then the term is underlined within 300ms And a tooltip suggests the canonical term And Publish remains enabled Given a glossary with canonical mappings and locked phrases And Feed enforcement mode is Auto-correct When the user clicks Publish with one or more violations Then the composer shows a change summary modal listing each replacement And upon confirm the post content is auto-corrected before submission And the submitted payload contains only normalized terms Given a glossary with canonical mappings and locked phrases And Feed enforcement mode is Block When the user attempts to publish with violations Then publishing is prevented And an error banner lists up to 5 offending terms with counts And focus scrolls to the first offending instance
Server-Side Middleware Enforcement for Content Creation APIs
Given per-surface modes are configured And the glossary is active When a POST is made to any of /api/feed/posts, /api/billing/invoices, /api/compliance/letters, or /api/notifications containing non-canonical terms And the surface mode is Auto-correct Then the middleware replaces variants with canonical terms and preserves locked phrases exactly And responds 201 Created with body.normalizedContent reflecting the changes And stores originalContent and normalizedContent separately Given per-surface modes are configured When a POST as above is made And the surface mode is Block Then the middleware rejects with 422 Unprocessable Entity And returns error.code="GLOSSARY_VIOLATION" And error.details[] includes {term, surface, positions[], policy, suggestion} Given per-surface modes are configured When a POST as above is made And the surface mode is Warn Then the middleware accepts the request with 2xx And includes header X-Glossary-Warnings with the count And response body.warnings[] enumerates the terms and positions Given any enforcement action occurs Then an audit log entry is written with {actorId, surface, action: warn|auto_correct|block, violationCount}
Import Endpoint Normalization and Violation Reporting
Given an admin uploads a CSV to /api/import/invoices with Preview=true And Billing surface mode is Auto-correct When rows contain non-canonical variants Then the preview response includes per-row diffs of replacements And summary.correctedCount equals the number of rows with changes And no rows are persisted Given an admin uploads a CSV to /api/import/compliance with Preview=false And Compliance surface mode is Block When any row contains violations Then the import is rejected with 422 Unprocessable Entity And a downloadable error CSV is provided including {rowNumber, field, term, policy, suggestion} Given an admin uploads a CSV with Preview=false And the target surface mode is Warn When rows contain violations Then all rows are persisted And the response includes summary.warningCount and a downloadable warnings report Given a row contains a locked phrase marked Do Not Translate When normalized output is generated Then the locked phrase is preserved byte-for-byte (including case and punctuation)
Template Rendering: Emails and PDFs Preserve Canonical and Locked Terms
Given an invoice with normalizedContent containing locked phrase "ACME HOA" and canonical term "Assessment" When generating email subject and HTML/text bodies Then both bodies contain "ACME HOA" exactly as defined And all related terms appear as "Assessment" And no alternative variants are present Given a compliance letter render to PDF When extracting text from the PDF Then locked phrases and canonical terms match exactly And are not split by hyphenation or line breaks within the phrase Given system locale differs from content locale And a term is marked Do Not Translate When rendering email and PDF Then the original term is preserved in both outputs regardless of locale Given identical inputs for a render operation When rendering twice Then the SHA-256 of the text content for both outputs is identical (deterministic rendering)
Per-Surface Enforcement Modes Configuration and Application
Given an admin with Manage Glossary permission updates enforcement modes to Feed=Warn, Billing=Auto-correct, Compliance=Block, Email=Auto-correct, PDF=Auto-correct, Push=Warn When Save is clicked Then the Settings API persists and returns the new modes And an audit event is recorded with the before/after values Given the above configuration When creating a feed post Then Warn behavior is applied; When creating an invoice Then Auto-correct is applied; When creating a compliance letter Then Block is applied; When sending email/PDF/push Then the configured mode for each surface is applied Given a global default mode=Warn And a surface-specific mode is not set When content is created for that surface Then the default mode is applied Given a user without Manage Glossary permission attempts to change modes When saving Then the request is denied with 403 Forbidden
Idempotent Processing on Repeated Sends Across Surfaces
Given a billing reminder is generated under Auto-correct mode And normalizedContent is stored with an idempotency key When the reminder email and push notification are retried or resent with the same idempotency key Then the stored normalizedContent is reused without additional changes And the content hash remains identical across sends Given the enforcement mode changes from Auto-correct to Block after the first send When the same reminder is resent without edits Then no re-evaluation occurs and the send proceeds; When the content is edited and resent Then the new mode is evaluated and enforced Given duplicate delivery retries from the notification service with the same messageId When processed by the middleware Then no duplicate warnings or corrections are logged And only one audit record exists for that idempotency key
Pre-Publish Compliance Gate & Exceptions Workflow
"As a compliance reviewer, I want a pre-publish checklist with clear fixes and an exceptions path so that we can move quickly without sacrificing accuracy."
Description

Before publishing or sending, run a fast glossary compliance scan that summarizes violations, suggested fixes, and impacted sections. Allow users with override permission to add a reason and proceed, creating an auditable exception. Provide a lightweight review queue for approvers to approve/deny exceptions and automated notifications to speed approvals. Store the compliance report with the content record for later auditing.

Acceptance Criteria
Pre-Publish Scan Blocks Noncompliant Content with Summary
Given a draft announcement, invoice, or compliance notice with Glossary Guard rules enabled When the author clicks Publish or Send Then the system runs a glossary compliance scan before any publish action And if violations are detected, a pre-publish dialog displays: - total violation count - each violated rule type - suggested fix text for each violation - impacted section identifiers (e.g., paragraph number and character range) And publishing is blocked until all violations are resolved or an authorized override is submitted And the scan completes within 2 seconds for content up to 10,000 characters in 95% of attempts
No-Violation Publish Flow
Given content is ready to publish and contains no glossary violations When the author clicks Publish or Send Then the content publishes immediately without invoking the exception workflow And a compliance report with zero violations is generated and stored with the content record
Override Permission and Auditable Exception Creation
Given violations are present on publish attempt And the current user has Override Exceptions permission When the user selects Override and Proceed Then the system requires an exception reason of at least 10 characters And if a valid reason is provided, the content is published And an auditable exception record is created linking: - submitter user ID - timestamp - content version identifier - list of overridden violations - entered reason And if the user lacks Override Exceptions permission, the Override and Proceed control is not visible
Approvals Queue Processes Exceptions
Given an exception record was created by an override When an approver opens the Review Queue Then the exception appears with status Pending, submitter, timestamp, and linked content And the approver can view the compliance report and a read-only snapshot of the published content When the approver clicks Approve Then the exception status changes to Approved and an audit entry is recorded When the approver clicks Deny Then the exception status changes to Denied and an audit entry is recorded
Automated Notifications for Exception Workflow
Given an exception is submitted Then all approvers in the designated group receive an in-app notification immediately and an email within 1 minute if email notifications are enabled And if the exception remains Pending for 24 hours, a reminder is sent to approvers, repeated every 24 hours up to 3 times or until decision When an exception is Approved or Denied Then the submitter receives an in-app notification immediately and an email within 1 minute if enabled
Compliance Report Stored with Content for Auditing
Given any publish attempt (with or without violations) When the compliance scan completes Then a compliance report is stored with the content record including: - scan timestamp - rule set version or hash - violation list (empty if none) - suggestions provided at time of scan - scanning user ID - content version identifier And the stored report is immutable and retrievable by admins via the content's Audit view
Automatic Re-Scan on Edits and Glossary Updates
Given a draft content item that previously passed a compliance scan When the content body is edited Then a new compliance scan runs on Save and again on Publish or Send attempt Given the Glossary Guard configuration changes after a draft was created When the draft is opened or Publish or Send is attempted Then a compliance scan runs using the updated rules And previously published items are not retroactively rescanned
Audit Trail, Versioning, and Historical Consistency
"As an auditor, I want a complete history of glossary rules and their application so that I can verify what recipients actually saw at the time of delivery."
Description

Record all glossary changes (who, what, when), including term edits, policy mode changes, and overrides. Version the glossary and stamp each published artifact (post, bill, notice) with the glossary version used, enabling reconstruction of the exact text at that time. Support diff views, exportable logs for auditors, and one-click rollback to prior glossary versions. Display in-document annotations showing where autocorrections occurred.

Acceptance Criteria
Publishing a Bill with Version-Stamped Glossary
Given an admin publishes a post, bill, or notice When the artifact is saved Then the artifact is stamped with the active glossary version ID and immutable checksum And the artifact’s rendered text is frozen to that version And the artifact metadata displays the glossary version and timestamp When the glossary is later updated Then the previously published artifact’s text does not change When the artifact is duplicated Then the user is prompted to keep the original glossary version or upgrade to the latest, and the choice is recorded in the audit log
Viewing Glossary Change Audit Log
Given a user with Audit permission opens the Glossary Audit Log When a term is added, edited, or deleted Then an append-only entry records actor ID, action type, term ID, old value, new value, timestamp (ISO 8601 UTC), and tenant ID When a policy mode change (e.g., lock/untranslate/translate behavior) occurs Then the log entry records previous mode, new mode, and scope When an override is applied to bypass a glossary rule for a specific artifact Then the log captures artifact ID, rule overridden, actor, justification note, and time When filtering the log by date range, actor, term, or change type Then results reflect the filter and are paginated and sortable And unauthorized users cannot access the log (HTTP 403)
Diff View of Glossary Versions
Given two selected glossary versions A and B When viewing the diff Then additions, edits, and removals are highlighted with counts by change type And changes include term text, canonical form, policy mode, protected flags, and translation rules And inline and side-by-side modes are available and accessible (keyboard navigable) When the diff exceeds 5,000 changed entries Then the view paginates without truncating results
Export Audit Logs for Auditors
Given an auditor requests an export When exporting the glossary audit log Then the system generates CSV and JSON files containing all selected fields with a fixed schema And the export is limited to the tenant’s data and selected date range And each file includes a cryptographic hash (SHA-256) and export metadata (exporter, time, version) And the export completes within 60 seconds for up to 100k entries or returns a queued job link And an audit entry records the export event And only users with Export permission can download (others receive HTTP 403)
One-Click Rollback to Prior Glossary Version
Given a user with Glossary Admin permission selects a prior version When clicking Rollback Then the system creates a new active version that is a copy of the selected version (history preserved) And requires a confirmation with rollback reason And records the rollback event in the audit log with actor, source version, new version ID, and reason And notifies watchers of the change When rollback is in progress Then concurrent edits are blocked or queued to prevent conflicts And previously published artifacts remain pinned to their original versions
Historical Reconstruction of Published Artifact Text
Given a published artifact stamped with glossary version V When viewing “As Sent” mode Then the system renders the exact text as of version V, including glossary autocorrections and casing And a banner displays “Using Glossary Version V” with link to diff from latest And a checksum of rendered content matches the stored publish-time checksum When attempting to edit the artifact Then the editor warns that changes will use the latest glossary unless explicitly pinned to V
In-Document Annotations of Autocorrections
Given an artifact with glossary-driven autocorrections When the viewer toggles Annotations On Then each autocorrected term is underlined and provides a tooltip showing original term, corrected term, rule name, and glossary version And annotations are visible only to authorized roles by default; recipients see clean text unless they enable annotations (if permitted) And annotations are keyboard- and screen-reader-accessible When exporting to PDF Then annotations render as footnotes or comment markers with the same details
Bulk Import/Export & API Integration
"As a platform admin, I want to bulk-manage glossary entries and integrate via API so that we can keep terms in sync with our legal sources and downstream tools."
Description

Support CSV/JSON import of terms from legal counsel or prior systems, with validation, duplicate detection, and dry-run previews. Provide export for backups and audits. Expose secure REST endpoints and webhooks for managing glossary entries and receiving violation events so that larger communities or partners can sync policies. Enforce rate limits and scoped tokens, and log all API interactions.

Acceptance Criteria
Bulk Import (CSV/JSON) with Validation & Dry-Run Preview
Given an admin uploads a CSV or JSON glossary file and selects Dry Run, when the system processes the file, then no data is persisted and a preview summary is returned including counts: totalRows, toCreate, toUpdate, duplicatesDetected, invalidRows, and a per-row error list with line numbers. Given the same file passes validation, when the admin runs Commit Import, then valid rows are persisted and invalid rows are skipped; the response includes importId and final counts; a downloadable error report for skipped rows is generated; each row write is atomic and retriable by row; the operation is idempotent when re-run with the same importId. Given UTF-8 encoded terms and attributes (e.g., category, locale, doNotTranslate), when imported, then all strings and flags are stored exactly as provided and normalized per system rules without unintended translation or case changes.
Duplicate Detection & Reporting
Given an import file, when the system computes a normalized key (trimmed, case-insensitive) over term + scope (e.g., locale/category), then duplicates within the file and against existing records are identified before commit and listed in the dry-run report with source row numbers and existing record identifiers. Given duplicates are detected during commit, when no explicit stable identifier/externalId links to an existing record, then duplicate rows are not created; they are skipped and reported; if an externalId matches, the existing record is updated rather than a new one created. Given the import completes, when the admin reviews results, then the system provides a deduplication report summarizing duplicatesFound, duplicatesSkipped, updatesApplied, and no two records share the same normalized key after the operation.
Glossary Export for Backups & Audits
Given an admin requests an export, when CSV or JSON is selected with optional filters (date range, category, locale), then the system generates a consistent snapshot including all relevant fields and metadata (id, term, scope, flags, createdAt, updatedAt, lastModifiedBy) and returns exportId and a short-lived, secure download link. Given an export is generated, when the file is downloaded, then the file is UTF-8 encoded, includes a header row for CSV, includes a SHA-256 checksum, and the exported record count matches the filters applied. Given any export action occurs, when auditing later, then the audit log includes who initiated the export, when, filters used, file format, record count, and whether the download link was accessed.
REST API for Glossary Management with Scoped Tokens & Rate Limits
Given a token with read:glossary scope, when the client calls GET /v1/glossary-entries, then the API returns 200 with paginated results; when the token lacks write:glossary scope for POST/PUT/PATCH/DELETE, then the API returns 403 with error code insufficient_scope. Given any request to the API, when sent over HTTP, then the request is rejected with 426/400; when sent over HTTPS with invalid input, then the API returns 400 with field-level error details; non-existent resources return 404; responses include a stable requestId header. Given per-token rate limits are configured, when the client exceeds the limit, then the API returns 429 with Retry-After and X-RateLimit-* headers; below the limit, headers reflect remaining quota; limits are enforced per token and tenant. Given POST requests include Idempotency-Key headers, when the same key is retried within the idempotency window, then only one resource is created and subsequent retries return the original response metadata.
Outgoing Webhooks for Glossary Events
Given a glossary entry is created, updated, or deleted, when the transaction commits, then subscribed endpoints receive a webhook event (glossary.created|glossary.updated|glossary.deleted) within the configured delivery window, containing eventId, occurredAt, tenantId, and resource data. Given a webhook is delivered, when the receiver validates the X-Signature HMAC-SHA256 header using the shared secret, then the signature matches the raw payload; invalid or missing signatures trigger retries and are logged. Given a webhook delivery fails (non-2xx or timeout), when retries are attempted, then exponential backoff is applied up to the configured max attempts; a 410 Gone response permanently disables the subscription; deliveries are idempotent by eventId to prevent duplicate processing.
Inbound Violation Events Endpoint with Security & Idempotency
Given a partner sends POST /v1/violation-events with a valid scoped token (write:violations) and a payload conforming to the published schema, when validated, then the API returns 202 Accepted with eventId and processing status queued; missing or invalid tokens return 401/403; schema violations return 400 with field-level errors. Given clients include an Idempotency-Key header, when duplicate submissions occur within the idempotency window, then no duplicate records are created and the original response is returned; events are processed exactly once. Given an event is received, when processing completes or fails, then the outcome is recorded in the audit log with correlation to eventId and requestId; sensitive fields are redacted at rest and in logs.
API Interaction Logging & Audit Trail
Given any import, export, REST API call, or webhook delivery occurs, when the action is executed, then an immutable audit record is created capturing timestamp, actor (userId/tokenId), tenantId, action, endpoint/resource id, requestId, response status, latency, and a payload hash with sensitive fields redacted. Given an admin with audit privileges queries logs, when filtering by time range, actor, action type, or resource id, then matching records are returned and can be exported as CSV/JSON with a checksum; access to logs is permission-gated. Given retention policies are configured, when logs reach their retention threshold (minimum 1 year), then logs are archived or purged per policy with a meta-entry recorded; audit records cannot be edited or deleted by end users.

Dual View

Instantly show original and translated content side by side or stacked. Builds trust, helps bilingual households, and simplifies dispute resolution and audit checks.

Requirements

Dual View Layout Toggle
"As a community member, I want to switch between side-by-side and stacked views of original and translated content so that I can read comfortably on any device and quickly verify meaning."
Description

Provide a control to display original and translated content side-by-side or stacked within posts, invoices, compliance notices, and messages across the Duesly feed. Persist the user’s last-selected layout per device, support responsive breakpoints for mobile/desktop, and maintain clear visual separation and labels for “Original” and “Translated.” Include keyboard navigation, print/export with both views, RTL-aware layout, and a change indicator when the original content is edited after translation to prevent misreads.

Acceptance Criteria
Layout Toggle Availability Across Content Types
Given a user opens any feed item of type Post, Invoice, Compliance Notice, or Message that has both original and translated content When the item is rendered Then a layout toggle control with options "Side by side" and "Stacked" is visible, enabled, and controls the dual view for that item Given a feed item does not have a translation available When the item is rendered Then the layout toggle is not displayed for that item Given the layout toggle is visible When the user switches between options Then the corresponding layout updates within 300 ms without page reload and without altering the underlying content
Layout Persistence Per User and Device
Given a signed-in user on Device A selects a layout option When the user opens any other eligible item on Device A Then the last-selected layout is applied by default for that user on Device A Given the same user signs in on Device B and has no prior selection on Device B When the user opens an eligible item on Device B Then the layout defaults based on responsive breakpoint rules until the user makes a selection on Device B Given User 1 and User 2 share Device A but use different accounts When each opens eligible items Then each user’s last-selected layout is applied independently on Device A
Labeling and Visual Separation
Given dual view is displayed in either layout When the panels render Then each panel displays a visible label exactly "Original" and "Translated" and each panel is programmatically associated with its label (aria-labelledby) Given dual view is displayed When inspected for visual separation Then a clear separation is present (e.g., divider or gutter) with at least 8 px spacing and a divider contrast of at least 3:1 against the background Given dual view is displayed When color contrast is measured Then all label text meets WCAG 2.2 AA contrast (>= 4.5:1)
Keyboard Navigation and Accessibility
Given a keyboard-only user focuses the feed item When navigating via Tab/Shift+Tab Then focus moves to the layout toggle and then to each labeled panel region in a logical order with a visible focus indicator (>= 2 px, 3:1 contrast) Given the layout toggle has focus When the user presses Space or Enter Then the selected layout changes and the change is announced to assistive technologies with the new state (e.g., "Layout: Side by side") Given each panel is rendered When a screen reader reads the regions Then each is announced with its label ("Original" or "Translated") and correct language attributes (lang) applied to content
Print and Export Include Both Views
Given a user prints an eligible item with dual view enabled When the print preview is generated Then both Original and Translated content are included with their visible labels and clear separation, without truncation of content Given a user exports to PDF using the app’s export function When the PDF is generated Then both Original and Translated content are included with labels and separation, matching the on-screen order for the selected layout Given side-by-side is selected and the printable page width cannot fit both panels at >= 320 px each When generating print or export output Then the output automatically uses stacked layout while preserving labels and content completeness
RTL-Aware and Responsive Layout Behavior
Given the translated content language is right-to-left When dual view renders Then the Translated panel content uses dir="rtl" and right-aligned typography while the Original panel retains its native direction, and labels remain correctly readable Given viewport width <= 768 px (mobile breakpoint) When dual view renders in stacked layout Then panels stack vertically full-width without horizontal scrolling and maintain labels and separation Given viewport width >= 1024 px (desktop breakpoint) When dual view renders in side-by-side layout Then panels display in two columns without overlap, each panel width >= 320 px, and no horizontal scrollbar appears in the page
Change Indicator After Original Edit
Given a translation exists for an item And the Original content is edited after the translation timestamp When the item is opened Then a visible change indicator appears in the dual view header stating that the Original was updated after translation, with the edit timestamp Given a change indicator is shown When a screen reader encounters the indicator Then the indicator is announced with role=status or equivalent and includes the timestamp Given the translation is refreshed to reflect the latest Original When the item is re-opened Then the change indicator no longer appears on screen or in print/export
Machine Translation Integration & Language Coverage
"As a board member, I want posts to be automatically translated into residents’ languages so that everyone can understand announcements and bills without delay."
Description

Integrate with one or more translation providers via an abstraction layer to translate content at publish-time and on-demand, covering the top target languages used by communities. Support automatic language detection for the source, configurable target languages per community, and a terminology glossary to preserve HOA-specific terms (e.g., “special assessment,” “violation,” “late fee”). Display provenance badges (e.g., “Auto-translated”) and handle failures gracefully with retries and user-facing fallbacks without blocking post visibility.

Acceptance Criteria
Publish-Time Translation Does Not Block Visibility
Given a community with target languages configured and a post authored in a single source language When the author publishes the post Then the post is immediately visible in the original language without delay And translation jobs are enqueued for each configured target language And when translations complete, they are attached to the post and retrievable via API and UI And pending translations display a visible "Translating" indicator for the target language And translation entries display a provenance badge labeled "Auto-translated"
On-Demand Translation by Reader
Given a published post and a viewer selects a target language that is not yet available or is outdated When the viewer requests a translation for that language Then the system invokes the translation abstraction layer for that language And upon success, the translation is displayed in Dual View for the selected language And the translation is cached for subsequent views And the translation displays a provenance badge including provider name And if the post content changed since the last translation, a fresh translation is generated and the prior one is invalidated
Automatic Source Language Detection with Confidence Threshold
Given post content provided in any supported language When the system processes the content for translation Then the system detects and stores the source language with a confidence score And if confidence is greater than or equal to 0.80, that language is used as the source And if confidence is less than 0.80, the system does not auto-translate and prompts the author to confirm or select the source language while keeping the post visible in the original language And the final source language selection is recorded in metadata and exposed via API
Community-Level Target Language Configuration
Given a community admin configures target languages in settings When a post is published or an on-demand translation is requested Then translations are generated only for the configured target languages And the language picker shows only the configured target languages to readers And configuration changes take effect for new translation jobs without deployment And the API returns the configured target language list for the community And attempting to request a non-configured language returns a 403 with an explanatory message
Glossary Enforcement for HOA Terminology
Given a terminology glossary with entries for source and target languages When translating content that contains glossary terms Then the output contains the glossary-specified terms exactly as configured, including case and plurality rules where specified And terms marked Do Not Translate remain unchanged in the output And translation metadata indicates whether the glossary was applied And if the provider cannot honor the glossary, the system retries with a provider that supports glossary; if none do, the translation is delivered with a visible notice Glossary not applied and provenance reflects the limitation And automated tests cover at least 20 glossary entries across supported languages with 100% pass rate
Provenance Badges and Audit Trail
Given translated content is displayed in the UI When the translation is rendered Then a visible badge indicates provenance as Auto-translated, Human-edited, or Provider fallback And the badge includes accessible labels describing provider and timestamp And interacting with the badge reveals details including provider name, timestamp, source and target languages, glossary applied status, and detection confidence And an audit log entry is created for each translation event capturing provider, response status, retries, fallback usage, duration, and glossary status And audit data is retrievable via admin reports and API with filters for community and date range
Graceful Failure Handling with Retries and Fallback
Given the primary translation provider returns an error or times out When a translation request is made Then the system retries up to two times with exponential backoff And if failures persist, the request is routed to a configured fallback provider And if all providers fail, the UI shows the original content with a non-blocking notice Translation unavailable and a retry option, and the post remains fully visible And failures are logged with error codes and correlation IDs And if a later retry succeeds, the translation replaces the notice without requiring re-publish
User Language Preferences & Localization Support
"As a resident, I want my language preferences respected so that I automatically see content in a way that’s readable and accessible for my household."
Description

Allow users to set preferred primary and secondary languages and auto-expand Dual View when a post’s language differs from their preference. Localize Dual View UI controls, labels, and empty states; support RTL scripts, accessible reading order, and screen-reader-friendly semantics. Respect household-level preferences for shared accounts, and apply locale formatting for dates, currencies, and numbers within translated bills and notices.

Acceptance Criteria
Auto-Expand Dual View When Post Language Differs From User Preference
Given a user has primary language set to "en" and secondary language set to "es" And the user opens a post detected as "es" When the post loads Then Dual View renders expanded by default showing original "es" and translated "en" content side-by-side (or stacked if viewport < 768px) And the Dual View toggle reflects "Expanded" state And collapsing or expanding persists per user per post for 30 days And if the post language equals the user's primary language, Dual View defaults to collapsed And if post language differs from both primary and secondary, Dual View expands with translation into the user's primary language
Primary and Secondary Language Preference Settings Per User
Given a signed-in user opens Language Preferences When the user selects a primary and optional secondary from the supported languages list Then the selection is validated against supported locales and saved to the user profile within 1 second And the setting syncs across devices within the same account on next session start And UI copy throughout Dual View immediately reflects the new preference without reload And removing the secondary language reverts Dual View auto-expand rules to primary-only behavior And audit logs record the change (timestamp, user id, old/new values)
Household-Level Language Preferences for Shared Accounts
Given a household account with multiple members and a configured household language preference When any member views shared bills/notices in Dual View Then the default translation target uses the household primary language And if a member has a different personal primary language, personal direct messages use the member's preference while household content uses the household preference And the household admin can edit the household preference; non-admins cannot And changes to household preference take effect for all members' shared content within 1 minute And the UI indicates which preference (household or personal) was applied for the current view
Localization of Dual View UI Controls, Labels, and Empty States
Given the user's UI language is set to "fr-CA" When the user opens any Dual View component Then all buttons, labels, tooltips, empty states, and error messages in the component are localized to "fr-CA" And there are no missing translation keys (no fallback English strings) in production builds And language names are displayed in their endonym (e.g., "Français (Canada)") And pseudo-localization tests with 30% string expansion show no truncation or overflow And date/number placeholders in empty states follow "fr-CA" conventions
Right-to-Left Script Support and Accessible Reading Order
Given the user's primary UI language is RTL (e.g., "ar") When Dual View is displayed side-by-side Then reading order, caret movement, and focus traversal follow RTL conventions And the panel order places the primary-language panel on the right and the other panel on the left And icons and chevrons are mirrored appropriately; numeric content follows locale bidi rules And in stacked mode, the top panel is the primary-language panel with correct heading order And horizontal overflow indicators appear on the correct edges in RTL
Screen Reader Semantics for Dual View and Translated Content
Given a screen reader is active When the user navigates into Dual View Then each panel is a labeled region with role="region" and an accessible name including the language (e.g., "Translated English", "Original Spanish") And language changes are marked with correct lang attributes so pronunciation follows language rules And keyboard navigation allows moving between panels and controls using standard keys without traps And toggling Dual View does not trigger assertive live regions; state change is announced via aria-expanded And all interactive controls meet WCAG 2.2 AA focus visibility and minimum target size requirements
Locale Formatting in Translated Bills and Notices
Given a bill originally in "es-MX" with amounts in MXN and dates in DD/MM/YYYY And the viewer's locale is "en-US" When the translated bill is rendered Then dates are formatted per "en-US" conventions (e.g., "Jan 5, 2026") And numbers use "en-US" grouping and decimal separators And currency amounts display the original currency with symbol and ISO code where ambiguous (e.g., "MX$1,234.56 MXN") without value conversion And negative amounts, zero values, and percentages follow "en-US" formatting rules And non-breaking spacing is applied where required by the target locale to prevent line wraps in amounts
Human Translation Workflow & Overrides
"As a property manager, I want to approve human translations that override machine output so that legally sensitive notices are accurate and defensible."
Description

Enable managers or authorized translators to upload or enter human-verified translations per language for any post, invoice, or notice. Provide draft/review/approved states, side-by-side comparison with machine output, and per-language publishing so communities can replace or override machine translations selectively. Surface a “Human-verified” label with timestamp and approver, and notify watchers when a human translation supersedes machine output.

Acceptance Criteria
Authorized Translator Submits Human Translation for a Notice
Given I am a Manager or Authorized Translator and I open a Post/Invoice/Notice with source language S And a human translation for target language L does not yet exist for this item When I open the translation editor, select L (ISO 639-1 with optional region), and enter translation text of at least 1 character or upload a file to populate the text And I click Save as Draft Then a Human Translation record is created in Draft for language L linked to the item and version 1 is recorded And unauthorized users receive a 403 and no record is created And duplicate creation for the same item and language is blocked with a clear error instructing to edit the existing draft
Review and Approval Workflow for Human Translations
Given a human translation exists in Draft for language L When a Reviewer or Manager opens it and clicks Submit for Review Then its status changes to In Review and a timestamp and submitter are logged Given a translation is In Review When an authorized Reviewer or Manager approves it Then its status changes to Approved, approver and ISO 8601 timestamp are recorded, and an audit entry is created When a Reviewer requests changes Then the status returns to Draft with a mandatory comment saved When editing an Approved translation Then a new Draft version N+1 is created; the prior Approved version remains active until the new version is Approved
Per-Language Publishing and Override of Machine Translation
Given a human translation for language L is Approved but not published When a Manager or Reviewer toggles Publish for L on the item Then the platform serves the human translation for L in all end-user views for that item, and machine translation is no longer shown for L And the publish action is recorded with timestamp and actor Given there is no Approved human translation for L Then the platform continues to serve machine translation for L When Publish for L is toggled off Then the platform reverts to serving machine translation for L
Dual View Comparison of Original, Machine, and Human Translation
Given a content item has a machine translation for L and a human translation (any state) for L When I open Dual View for L on desktop Then the Original and the selected translation are displayed side-by-side with clear headers (Original [S], Human [L] or Machine [L]) And on viewports under 768px, the panels are stacked vertically in the same order And I can switch between Machine and Human translations for L without page reload Given there is no human translation for L Then Dual View shows Original [S] and Machine [L] only
Human-Verified Label, Timestamp, and Approver Attribution
Given a human translation for L is Approved and Published When it is rendered in feed, detail view, invoice PDF, or notice Then a 'Human-verified' label is shown adjacent to the translation, with approver display name and ISO 8601 approval timestamp accessible to the user And the label is not shown on machine translations or unpublished/draft human translations And the attribution is recorded in the item's audit log
Watcher Notifications on Human Override
Given a user is a watcher of an item and has notifications enabled When an Approved human translation for language L is Published (overriding machine) for that item Then the watcher receives a notification within 1 minute via their preferred channels containing the item title, language L, 'Human-verified' indicator, approver name, and a link to view And duplicate notifications for the same user/item/language within one hour are suppressed And a notification event is logged with delivery outcome per channel
Translation Provenance & Audit Logging
"As a board secretary, I want a complete audit trail of translations so that I can resolve disputes and satisfy audit requests with verifiable evidence."
Description

Record immutable provenance for each translation: original content hash, source language, translation method (provider or human), model/version, glossary used, approver identity, timestamps, and change history. Provide exportable, read-only artifacts (PDF/CSV) that include both original and translated content with provenance, and surface these entries in Duesly’s existing compliance/audit logs to aid dispute resolution and regulatory checks.

Acceptance Criteria
Capture Immutable Translation Provenance on Creation
Given a user creates a translation in Dual View for a content item When the translation is saved Then the system writes a provenance record containing: content_item_id, translation_id, original_content_sha256, source_language, target_language, translation_method (human|provider), provider/model_version (or human_identifier), glossary_id/version (nullable), approver_user_id (nullable), created_at (UTC ISO-8601) And the record is immutable (no UPDATE allowed); only append-only history entries are permitted And any attempt to modify the record in place returns HTTP 409 and no data is changed And original_content_sha256 verifies against the current original content at read time
Append-Only Change History for Translation Revisions
Given a translation is edited, re-generated, glossary changed, or approved When the change is committed Then a change_history entry is appended with: previous_version_id, action_type, reason (optional), actor_user_id, timestamp (UTC), diff_summary, and prev_hash/current_hash to form a verifiable chain And no prior history entry is altered; total count increases by 1 And recomputing the hash chain validates all entries And the UI exposes a read-only list of versions with timestamps and actors
Export Read-Only PDF Artifact with Provenance
Given an Admin or Manager views a translation in Dual View When Export PDF is invoked Then a PDF is generated embedding original and translated text in the current layout (side-by-side or stacked) plus a provenance section (all fields and history summary) And the PDF contains artifact_id, generated_at (UTC), and a printed SHA-256 checksum of the PDF bytes And the PDF is flattened, embeds fonts, and disallows editing/annotation (standard permissions) And the download event is logged with event_type=translation.export.pdf, user_id, artifact_id, timestamp
Export Machine-Readable CSV Artifact with Provenance
Given an Admin or Manager invokes Export CSV for a translation When the CSV is generated Then the CSV uses UTF-8 (no BOM), LF line endings, RFC 4180 quoting, and includes headers: artifact_id, content_item_id, translation_id, original_content_sha256, source_language, target_language, translation_method, provider, model_version, glossary_id, glossary_version, approver_user_id, created_at, approved_at, version, change_id, change_action, change_actor_user_id, change_reason, change_timestamp, original_text, translated_text And change history is represented as one row per change with shared artifact_id and translation_id And the file passes schema validation and opens without corruption in Excel, Google Sheets, and LibreOffice And the export event is logged with event_type=translation.export.csv
Surface Translation Entries in Compliance/Audit Logs
Given translation-related events occur (create, approve, edit, export) When the audit log is queried by an Admin/Manager Then corresponding entries appear in the existing compliance/audit feed within 5 seconds of the event And entries are filterable by event_type prefix translation.*, date range, user_id, content_item_id, language, and artifact_id And entries redact resident PII by default and show timestamps in UTC And retention obeys the organization’s audit retention setting
Dual View UI Displays Provenance Summary and Verification
Given a user opens Dual View for a translated content item When the provenance panel is expanded Then it shows source_language, target_language, translation_method, provider/model_version, glossary_id/version, approver (if any), created_at/approved_at, and original_content_sha256 with copy-to-clipboard And a verification badge indicates Hash Verified when the original content matches the stored hash; otherwise shows Mismatch with tooltip And a View Full History action reveals the append-only change list And the panel meets WCAG 2.1 AA for keyboard navigation and labels
Role-Based Access Controls for Provenance and Exports
Given role-based permissions (Resident, Board Admin, Manager, Compliance Viewer) When a user without export permission views Dual View Then provenance is view-only and Export PDF/CSV controls are hidden and API calls to export return HTTP 403 and are logged And users with Admin/Manager/Compliance Viewer roles can export; links are single-use and expire after 15 minutes And access to audit log entries is limited to Admin/Manager/Compliance Viewer; Residents cannot access them And all access and denial events are recorded with user_id and timestamp
Performance, Caching & Spend Controls
"As an admin, I want translations to be fast and within budget so that residents get timely information without unexpected costs."
Description

Pre-translate at publish-time for configured languages, cache results with invalidation on content edits, and lazy-load additional languages on demand to keep the feed fast. Implement rate limiting and batching to respect provider quotas, track character usage by community, enforce monthly spend caps with alerts, and fallback to on-demand translation when budgets are reached—without degrading access to the original content.

Acceptance Criteria
Publish-Time Pre-translation & Cache Warmup
Given a post is published in source language L0 and the community has configured target languages {L1..Ln} When the post is published Then translations for all configured languages are enqueued within 2 seconds, executed, and cached with cacheVersion = 1 Given pre-translation completes When any user opens the feed and selects a configured language Then the translation is served from cache (no provider call) and the dual view renders within 200 ms per post on average Given content length exceeds provider single-request limits When pre-translation runs Then the content is chunked, batched (<= 50,000 characters per request or provider max, whichever is smaller), reassembled in order, and stored as a single cache entry preserving paragraph breaks
Cache Invalidation on Edit and Versioning
Given the author edits and saves the original text of a post When the save succeeds Then all cached translations for that post are marked stale and invalidated within 1 second Given translations were invalidated due to an edit When background re-translation completes Then a new cacheVersion is written (previous becomes superseded) and subsequent reads serve the new version Given only non-text metadata (e.g., tags, visibility, pin state) is changed When the post is saved Then translation cache entries remain valid (no invalidation occurs)
Lazy-Load Additional Languages On Demand
Given a user selects a non-configured language Lx in Dual View When no cached translation for Lx exists Then an on-demand translation request is sent and a non-blocking loading indicator is shown while original content remains visible Given the user scrolls and new posts enter the viewport When requesting translations for Lx Then requests are batched up to 50,000 characters per batch and limited to configured concurrency of <= 4 in-flight batches Given provider latency exceeds 2 seconds When on-demand translation is pending Then the UI maintains layout with cumulative layout shift < 0.1 and inserts the translation progressively without shifting original content more than 100 px
Provider Rate Limiting and Batching
Given multiple translation requests are queued When provider limits are configured as R requests/second and C characters/second Then the client respects these limits by batching and throttling so that instantaneous throughput never exceeds R or C Given the provider responds with HTTP 429 (rate limited) When retries are attempted Then exponential backoff is applied with jitter for up to 3 retries, and the UI shows a delayed state without error banners Given a mixed batch returns partial successes When results are processed Then successful items are cached immediately, failed items are retried independently, and no duplicate provider calls are made for successful segments
Per-Community Character Usage Tracking
Given any translation request is sent to the provider When the request is finalized Then the system records the exact billed character count against the community, language pair, and feature (pre-translate vs on-demand) within 60 seconds Given an admin views the usage dashboard for the current month When data is displayed Then totals per community and per language pair match the provider report within ±1% and include a downloadable CSV Given the billing period rolls over at 00:00 UTC on the first day of the month When the new period starts Then usage counters reset to zero and the previous period is archived and still accessible for export
Monthly Spend Cap Enforcement and Alerts
Given a community sets a monthly spend cap C and an alert threshold T% When cumulative billed spend reaches T% of C Then in-app and email alerts are sent to all billing admins within 5 minutes and are logged with timestamp Given cumulative spend reaches 100% of C in the current period When additional translations are triggered automatically (pre-translate or background refresh) Then those jobs are not executed against the paid provider, no spend is added, and the system logs a cap-enforced skip Given the cap is reached and a user explicitly requests a translation on demand When a cached translation exists Then it is returned immediately; otherwise the request is queued for execution after the cap resets and the UI displays "Translation queued; original available" without blocking access to the original
Dual View Performance SLAs & Graceful Degradation
Given a feed with up to 50 posts and cached translations for configured languages When a user opens Dual View Then time-to-first-render is < 1.5 seconds on a simulated 4G profile and main-thread blocking time is < 100 ms per frame Given translations are unavailable due to provider outage or spend cap When Dual View is toggled Then original content is always displayed with a subtle notice, and no spinner persists longer than 1 second Given a translation cache miss occurs When the user navigates away before completion Then the in-flight request is canceled or reused (deduplicated) to prevent duplicate spend, and no orphan cache entries are written

Reply Translate

Comments and DMs auto-translate to the recipient’s preferred language while displaying in the sender’s. Keeps ARC discussions and violation follow-ups clear without extra steps.

Requirements

Preferred Language Management
"As a board member or resident, I want to set my preferred language so that I always receive posts, comments, and DMs in a language I understand."
Description

Introduce profile-level preferred language settings with sensible defaults (browser locale) and optional per-community and per-thread overrides. Persist selections in user profiles and expose them via API so messaging, comments, notifications, and feeds can resolve each viewer’s target language. Update onboarding, settings UI, and service layer to read these preferences and enforce validation against a supported-languages list. Provide migration to backfill existing users with defaults and telemetry to track adoption.

Acceptance Criteria
Onboarding Default Language from Browser Locale
Given a new user with no stored language preferences and browser locale 'es-ES' where 'es' is supported, When onboarding begins, Then the language picker defaults to 'es' and will be saved to profile on account creation. Given a new user with unsupported browser locale 'fy-NL', When onboarding begins, Then the language picker defaults to platform default 'en' and only supported languages are selectable. Given the user completes onboarding, When the account is created, Then user.profile.preferred_language equals the selection and updated_at is set. Given the user abandons onboarding before completion, When the session ends, Then no preferred language is persisted.
Settings and API Persistence for Profile Language
Given an authenticated user, When they change their profile-level preferred language to 'fr' in Settings and click Save, Then the value persists and GET /api/v1/users/{id}/preferences returns preferred_language = 'fr'. Given the profile-level language is changed, When the user reloads any page, Then UI copy and translation targets resolve to 'fr' without requiring re-login. Given a transient server error occurs on save, When the user clicks Save, Then a non-destructive error is shown, the previous value remains, and no change is returned by the API.
Language Resolution Order Across Scopes
Given a user viewing a thread with a thread-level language override 'de', When the system resolves target language, Then it uses 'de' regardless of community or profile settings. Given no thread-level override but a community-level override 'pt-BR' exists, When resolving target language, Then it uses 'pt-BR'. Given neither thread nor community overrides exist and profile-level is 'fr', When resolving target language, Then it uses 'fr'. Given no overrides and no profile-level preference, When resolving target language, Then it uses platform default 'en'. Given the service layer exposes language resolution, When GET /api/v1/language/resolve?user_id={u}&community_id={c}&thread_id={t} is called, Then it returns the same language code used for rendering/messaging under the above cases.
Supported Languages Validation and Errors
Given the supported languages list is ['en','es','fr','de','pt-BR'], When a user attempts to save 'it' via UI or API, Then the request is rejected with HTTP 400 and error_code = 'unsupported_language' and no preference is changed. Given the supported languages list updates at runtime, When a previously valid override becomes unsupported, Then the next resolution falls back to the next scope or platform default and emits telemetry event 'language_unsupported_fallback'. Given an API client requests the supported list, When GET /api/v1/languages/supported is called, Then it returns the current canonical list including locale codes and labels.
Per-Community Preferred Language Override
Given a user belongs to Community A and Community B, When they set a community-level override 'es' for Community A, Then viewing feeds, comments, DMs, and notifications scoped to Community A resolve to 'es' and Community B remains unaffected. Given a community-level override exists, When the user removes it, Then subsequent resolutions for that community fall back to profile-level or platform default per the resolution order. Given a community admin changes supported languages to exclude the override, When the user next views Community A, Then target language falls back and a non-blocking notice is shown.
Per-Thread Preferred Language Override
Given a user is participating in a specific ARC thread, When they set a thread-level override to 'de', Then messages and comments in that thread resolve to 'de' for that user without affecting other threads or communities. Given a thread-level override exists, When the user removes it, Then subsequent messages/comments in that thread resolve using community or profile settings. Given multiple recipients have different thread overrides, When a new comment is posted, Then each recipient sees the content translated into their own resolved language while the stored original remains unchanged.
Backfill Migration and Telemetry
Given existing users without a preferred_language, When the migration runs, Then each user is assigned the last-known browser locale if supported else 'en', and users with an existing value are not modified. Given the migration is re-run, When executed again, Then it is idempotent and does not change any previously set preferences. Given any preference is set or changed at profile, community, or thread scope, When the action completes, Then telemetry event 'language_preference_set' is emitted with fields {user_id, scope, old_value, new_value, source, timestamp}.
Auto-Translation Engine Integration
"As a resident receiving a direct message, I want it translated automatically into my language so that I can respond without using external tools."
Description

Integrate with a machine translation provider with automatic source language detection and programmatic selection of the target language per recipient. Build a service that queues translation jobs, handles rate limits and retries, and supports provider failover. Ensure secure transmission, secrets management, observability (latency, error rate, cost), and standardized error responses. Normalize provider outputs to a common schema including source/target locales, confidence (when available), and token usage to enable cost monitoring.

Acceptance Criteria
Auto-detect Source Language per Message
Given a message without an explicit source locale, When the translation service receives it, Then it detects the source locale and returns it in sourceLocale as a valid BCP-47 tag. Given a curated test set of 200 messages across 10 languages, When processed, Then >= 95% have correctly detected sourceLocale. Given the detection confidence is below 0.60, When processing a translation request, Then the service sets sourceLocale to "und" and returns an error with code "LOW_CONFIDENCE" and isRetryable=false.
Target Language Selection per Recipient Preference
Given a recipient with preferred locale set (e.g., "es-MX"), When a message is delivered, Then targetLocale equals the recipient’s preferred locale regardless of the sender’s locale. Given multiple recipients with different preferred locales, When a single message is sent, Then the service produces and returns separate translations per recipient-locale pair. Given a recipient without a preferred locale, When processing, Then targetLocale defaults to tenantDefaultLocale and the audit log records reason="NO_PREFERRED_LOCALE". Given the requested target locale is unsupported by all providers, When processing, Then the service returns an error with code "UNSUPPORTED_LOCALE" and isRetryable=false.
Queueing, Idempotency, and Rate-limit-aware Retries
Given two requests with the same idempotencyKey within a 24-hour window, When enqueued, Then only one job is executed and both requests receive the same result payload. Given the provider responds with HTTP 429 or a documented rate-limit signal, When processing a job, Then the worker retries with exponential backoff (200ms base, jitter, max 3 attempts) and includes retryCount in metadata. Given a worker crashes mid-processing, When the service restarts, Then the job is recovered from the queue and is not executed more than once (at-least-once with idempotency guarantees). Given 100 concurrent translation requests, When enqueued, Then the system maintains p95 end-to-end latency ≤ 1.5s and zero lost jobs under synthetic load for 10 minutes.
Provider Failover with Health Checks and Circuit Breaker
Given the primary provider exhibits >= 10% 5xx or >= 5% timeout rate over a rolling 1-minute window for a language pair, When detected, Then the circuit opens within 5 seconds and new jobs for that pair use the secondary provider. Given the primary provider error/timeout rates remain below thresholds for 5 continuous minutes, When monitored, Then the circuit closes and traffic is restored gradually (≥ 20% increments per minute) to prevent thrashing. Given no provider supports the requested language pair, When processing, Then the service returns error code "UNSUPPORTED_LOCALE" with isRetryable=false and provider=null. Given failover occurs, When the job completes, Then provider used is reflected in the response metadata and in the audit log.
Secure Transmission and Secrets Management
Given any outbound call to a translation provider, When invoked, Then TLS 1.2+ with certificate validation is enforced and plaintext transport is blocked. Given API keys and secrets, When the service runs, Then secrets are stored in a managed secret store (e.g., KMS/Vault), loaded at runtime, not hardcoded, rotated at least every 90 days, and rotation requires no downtime. Given logs and traces, When messages are processed, Then raw message text is redacted/truncated and secrets are never logged. Given messages are persisted in queues or DLQs, When stored at rest, Then data is encrypted using managed encryption (e.g., AES-256) and access is restricted by least privilege IAM. Given a secret rotation event, When executed, Then the service continues to function and emits an audit log event with actor, timestamp, and key identifier.
Observability: Latency, Error Rate, and Cost Metrics with Alerts
Given translation requests are processed, When metrics are scraped, Then the service emits request_count, success_count, error_count by error.code, p50/p95/p99 latency, cost_usd, token_usage_input, token_usage_output, provider, and language_pair. Given normal operation, When viewed in dashboards, Then per-tenant and per-provider daily cost and token usage are visible with 30-day retention. Given p95 latency > 2.0s for 5 consecutive minutes, When detected, Then an alert is sent to on-call within 2 minutes. Given error rate > 2% over a rolling 5-minute window with ≥ 200 requests, When detected, Then an alert is sent to on-call within 2 minutes. Given any request, When logged, Then logs include correlationId, jobId, provider, language_pair, attempt, durationMs, and outcome without exposing message content.
Normalized Output and Error Schema with Confidence and Token Usage
Given a successful provider response, When returned to callers, Then it conforms to the schema: {sourceLocale (BCP-47), targetLocale (BCP-47), translatedText, provider, providerVersion, detectedSourceConfidence (0..1|null), tokenUsage:{input, output, unit in ["tokens","chars"], derived:boolean}, processingLatencyMs, correlationId}. Given a provider omits confidence, When normalized, Then detectedSourceConfidence=null. Given a provider omits token usage, When normalized, Then tokenUsage is populated by a deterministic estimator and tokenUsage.derived=true. Given inputs include non-canonical locale tags, When normalized, Then tags are canonicalized to BCP-47 (e.g., "pt-br" -> "pt-BR") or rejected with error code "INVALID_LOCALE" before provider calls. Given any error occurs, When returned, Then it conforms to {error:{code in ["LOW_CONFIDENCE","UNSUPPORTED_LOCALE","RATE_LIMIT","PROVIDER_ERROR","TIMEOUT","INTERNAL","INVALID_LOCALE"], message, httpStatus, isRetryable:boolean, correlationId, provider|null}}.
Per-Recipient Delivery and UI Rendering
"As a community manager posting an ARC comment, I want each participant to see my message in their preferred language so that the discussion stays clear for everyone."
Description

Render each message and comment per viewer using the viewer’s preferred language while the sender continues to see their original text. Support multi-recipient threads, group DMs, and ARC comment streams by storing the original content plus per-locale translations and selecting the correct variant at render time. Extend notification pipelines (push, email) to include the recipient’s translated content. Ensure consistent behavior across web and mobile clients and preserve formatting, mentions, links, and attachments.

Acceptance Criteria
1:1 DM – Per-Recipient Translation and Sender Original
Given user A (pref: en) sends a DM message in English to user B (pref: es) When B views the thread on any client Then B sees the Spanish translation with preserved formatting, mentions, links, and attachments And A continues to see the original English text And reactions, timestamps, and message IDs are identical for both users And the translation is generated within 2 seconds of message send
Group DM – Multi-Recipient Language Rendering
Given user A (pref: en) sends a message to a group containing users B (pref: es) and C (pref: fr) When each recipient opens the thread Then B sees a Spanish translation, C sees a French translation, and A sees the original English And mentions resolve to the correct user IDs for all recipients And link previews and attachment thumbnails are preserved for all recipients And reactions and threading behave identically across language variants And each translation is cached and reused for subsequent views and notifications
ARC Comment Stream – Per-Viewer Translation
Given an ARC post where commenter X (pref: de) adds a comment containing @mentions, numbered lists, and a photo with a caption When viewers with preferences en, es, and de open the post Then en and es viewers see translated English and Spanish variants respectively, and the de viewer sees the original German And @mentions notify the correct users and display localized display names without altering mention targets And list formatting, inline styles (bold/italics), links, and photo captions are preserved across translations
Notifications – Push and Email Include Recipient Translation
Given A (pref: en) sends a DM to B (pref: es) and a new ARC comment is relevant to D (pref: fr) When push and email notifications are generated Then B’s push body and email subject/preview contain the Spanish translation And D’s notifications contain the French translation And opening the notification deep-links to the thread showing the same translated variant And no recipient receives the sender’s original text unless their preference matches the original language
Fallback – Unsupported Locale or Translation Failure
Given a message in Japanese and a recipient with preference Catalan (unsupported) or the translation service times out When the recipient views the content or receives notifications Then the system displays the original message with an unobtrusive “translation unavailable” indicator and does not block delivery And the system retries translation in the background up to 3 times within 15 minutes And once a translation becomes available, subsequent views and notifications use the translated variant
Cross-Client Parity – Web, iOS, Android
Given a user with preference Spanish opens the same message on web, iOS, and Android When the message is rendered on each client Then the displayed text, formatting, mentions, links, and attachments are consistent across clients And the selected language variant is identical on all clients for that user And read receipts and reaction counts remain consistent across clients and language variants
Edit/Delete – Translation Refresh and Invalidation
Given the sender edits the original message body When the edit is saved Then all stored translations are invalidated and regenerated from the updated original within 5 seconds And recipients subsequently see the updated translated text; prior translations are not shown again And when the sender deletes the message, all translations are removed and the deletion state is reflected consistently across clients and notifications
Original/Translated Toggle and Transparency Labels
"As a resident reading a translated violation follow-up, I want to view the original message and see which language it was translated from so that I can verify nuance and trust the message."
Description

Provide an inline toggle to switch between translated and original text, with a visible label indicating the source language and a brief accuracy disclaimer. Persist the user’s last choice per thread. Display translation status (e.g., loading, failed) and fall back gracefully to the original when needed. Ensure accessibility (screen reader labels, keyboard focus) and consistent placement across comments, DMs, and notification previews.

Acceptance Criteria
Inline Translate Toggle in Message View
Given a recipient whose preferred language differs from the message's source language and an available translation When the recipient taps/clicks the "View original" or "View translation" inline toggle for a specific message/comment Then the message text switches between original and translated without reloading the thread and within 300 ms after translation payload is available And the toggle label flips accordingly ("View translation" ↔ "View original") And only that specific message/comment switches (no other items affected) And the toggle remains in the same location and preserves vertical layout shift ≤ 8 px
Persist Last Toggle Choice Per Thread
Given a user sets their view to "Original" or "Translated" on any item within a thread (comment chain or DM conversation) When the user reopens that same thread on the same or another device while logged in Then the last choice is applied to all newly rendered items in that thread by default And the preference is stored per thread ID and per user for at least 30 days And changing the choice updates the stored preference immediately
Source Language Label and Accuracy Disclaimer
Given a message/comment contains translated content When it is displayed in either original or translated mode Then a visible label shows "Translated from <Language>" in translated mode or "Original in <Language>" in original mode using the language name localized to the viewer And a brief accuracy disclaimer (≤ 80 characters) is shown adjacent to the label And if the source language is unknown, the label displays "Language unknown" And label text meets WCAG AA contrast and is announced by screen readers with its current state
Translation Status Indicators and Graceful Fallback
Given a translation request is in progress When the item renders Then a non-blocking inline "Translating…" status appears within 150 ms and skeleton/placeholder text is shown And if translation succeeds within 5 s, the translated text replaces the placeholder And if translation fails or times out after 5 s, the original text is shown with an inline "Translation failed" status and a retry affordance And retry triggers a new request without duplicating content
Accessibility: Screen Reader and Keyboard Operability
Given a user navigating via keyboard or assistive technology When focus reaches the toggle Then the toggle is reachable via Tab/Shift+Tab, has visible focus, role="button", and aria-pressed reflecting current state And the source language label and disclaimer have aria-live="polite" updates when state changes And all toggle actions are operable with Enter/Space and announced by NVDA, JAWS, and VoiceOver in tests And contrast ratio for controls and text is ≥ 4.5:1
Consistent Placement Across Comments, DMs, and Notification Previews
Given an item appears in comments, DMs, or notification previews When the item renders Then the toggle and label are positioned directly beneath the message text, right-aligned, with identical spacing tokens across surfaces And the same iconography and copy are used across all surfaces And in notification previews, toggling updates the preview text in place without opening the app when supported; otherwise, the control deep-links to the item with state preserved And no surface omits the label when translation is applied
Edit Handling, Versioning, and Cache Invalidation
"As a manager correcting a typo in a DM, I want translated versions to update automatically so that recipients don’t see conflicting information."
Description

Track message revisions and automatically retrigger translations when the original content changes. Cache translations by a hash of the original text and locale to avoid duplicate work and costs, and invalidate the cache on edit or deletion. Mark translations as outdated if the original changes while re-translation is pending. Ensure idempotent job processing, deduplication, and auditability by correlating translations with specific message versions.

Acceptance Criteria
Auto-Retranslate on Edit with Version Correlation
Given a message M at version v1 with completed translations for locales {L} When the sender saves an edit producing version v2 Then the system enqueues exactly one translation job per locale in {L} for version v2 And each job is correlated to messageId=M and versionId=v2 And translations for v1 remain stored and are not overwritten And no translation job is enqueued for locales not required by recipients of M
Translation Cache by Text-Locale Hash
Given original text T and target locale L When a translation is requested Then the system computes H = hash(T, L) And if a cache entry exists for H (provider/model/config matched), it serves the cached translation (cacheHit=true) without calling the external API And if no cache entry exists for H, it calls the provider, stores the result under H with provider/model/config metadata (cacheHit=false), and returns the translation And subsequent requests for the same T and L reuse the cached translation
Cache Invalidation on Edit or Deletion
Given message M with version v1 and cached translations keyed by H1 = hash(Tv1, L) When M is edited to version v2 with text Tv2 Then cache entries for H1 associated to M are invalidated And pending translation jobs for v1 are canceled And new translation jobs are enqueued for v2 for all required locales When message M is deleted Then all pending translation jobs for M are canceled And all cache entries associated to M (H1..Hn) are invalidated And translations for M are no longer returned by user-facing APIs
Idempotent Job Processing and Deduplication
Given multiple identical enqueue attempts for (messageId=M, versionId=v, locale=L) When the system processes translation jobs Then an idempotency key K = M|v|L is used to deduplicate work And at most one external translation API call is made per K And at most one translation record is persisted per K And retries or concurrent workers do not create duplicate jobs, records, or notifications And the final job status is exactly one of {queued, started, succeeded, failed} with a single terminal state per K
Outdated Flag While Re-translation Pending
Given recipients currently see a translation for (M, v1, L) When an edit creates version v2 and translations for L are pending Then the API marks the v1 translation as outdated=true And the UI can display an Outdated indicator for that translation And upon completion of (M, v2, L) translation, outdated is cleared and the v2 translation replaces v1 automatically And if the re-translation ultimately fails after final retry, outdated remains true and failure status is exposed via admin/observability endpoints
Auditability and Version-Scoped Traceability
Given any translation event for (messageId=M, versionId=v, locale=L) When the audit log is queried for M Then the system returns an immutable record containing at least {messageId, versionId, sourceLocale, targetLocale, sourceHash, idempotencyKey, cacheHit flag, provider, model, jobId, enqueuedAt, startedAt, completedAt, triggeredBy} And there is exactly one completed translation record per (M, v, L) that succeeded And canceled/failed attempts are present with distinct statuses and linked to the same idempotencyKey And records can be filtered by versionId to verify which translation corresponds to which message version
Admin Controls, Policies, and Compliance
"As a board admin, I want to control and audit translation behavior so that we meet community policies and privacy requirements."
Description

Add organization-level settings to enable/disable Reply Translate, restrict allowed target languages, and allow user opt-out. Present consent and privacy notices explaining third-party processing. Redact or protect structured sensitive tokens (e.g., payment amounts, unit numbers) before external transmission when required. Write audit logs capturing message ID, source/target languages, provider, timestamps, and cost metrics, and expose export endpoints for compliance reviews.

Acceptance Criteria
Org-Level Toggle: Enable/Disable Reply Translate
Given org setting Reply Translate = Disabled, When a message is posted between users with different preferred languages, Then no translation request is sent to any external provider and the recipient sees the original text only. Given org setting Reply Translate = Enabled, When a message matches translation conditions, Then translation occurs per policy and a translation indicator is shown to the recipient. Given an admin updates the toggle, When the change is saved, Then the setting takes effect for new messages within 60 seconds and is persisted across sessions. Given org setting = Disabled, When any client attempts to force translation, Then the API returns 403 with policy_blocked and makes no provider call.
Allowed Target Languages Policy
Given an allowlist of target languages is configured at org level, When the recipient's preferred language is in the allowlist, Then the system requests translation using that language code. Given the recipient's preferred language is not in the allowlist, When a message is delivered, Then no translation occurs and the system falls back to original text only. Given the allowlist is empty, When an admin attempts to save with feature enabled, Then validation fails with an error "At least one language required". Given the allowlist is changed, When saved, Then subsequent translations honor the new list immediately and existing stored messages are not altered. Given an invalid or duplicated code is entered, When saved, Then the system normalizes to supported ISO language codes and rejects unsupported codes with a clear error.
User Opt-Out of Auto-Translation
Given a user has opted out of auto-translation, When they receive a message in a different language, Then no translation request is made and the original text is delivered. Given a user has opted out, When they send messages to recipients who have not opted out, Then those recipients still receive auto-translations per org policy. Given a user toggles opt-out, When saved, Then the preference is effective within 60 seconds across web and mobile and is persisted. Given a user is opted out, When an admin attempts to override via API, Then the API denies with 403 user_opt_out and no provider call occurs.
Consent and Privacy Notices
Given an admin enables Reply Translate for the first time, When saving, Then a modal displays third-party processing notice naming providers and linking to the privacy policy, and explicit consent is required to proceed. Given end-user auto-translation will occur for the first time, When they next view an affected message, Then a one-time notice is displayed with consent options; until accepted, no translation requests are sent for that user. Given consent is captured, When stored, Then records include actor_id, timestamp, notice_version, org_id, and locale, and are retrievable via admin compliance view. Given consent is withdrawn, When user or admin revokes, Then subsequent auto-translation stops and a revocation record is saved with timestamp and actor.
Sensitive Token Redaction Before External Transmission
Given redaction is required, When a message contains structured tokens matching configured patterns (e.g., monetary amounts, unit numbers), Then those tokens are replaced by placeholders before calling the provider. Given placeholders are used, When a translation response is received, Then the system re-inserts the original tokens in the correct positions and format with 100% fidelity. Given a redaction failure occurs, When detection or reinsertion cannot be completed, Then the system aborts the provider call or discards the translation, delivers the original text, surfaces a non-blocking warning to the recipient, and logs the event. Given a provider payload is logged for debugging, When stored, Then it contains placeholders only and no raw sensitive tokens.
Audit Logging for Translation Events
Given any translation attempt (success or failure), When processed, Then an immutable audit log entry is created with message_id, org_id, sender_id, recipient_id, source_lang, target_lang, provider, request_timestamp, response_timestamp (or error_code), token_or_char_count, and cost_in_cents. Given audit data exists, When queried by authorized admins, Then results can be filtered by date range, org, user, language, provider, success/failure, and conversation_id. Given logs are stored, When retention is evaluated, Then entries are retained for at least 24 months and are tamper-evident with write-once semantics. Given clock skew, When timestamps are recorded, Then all are in UTC ISO 8601 with millisecond precision.
Compliance Export Endpoints
Given an org compliance admin is authenticated, When they call the export endpoint with a valid date range, Then the API returns a 200 with a downloadable file in the requested format (CSV or JSON) containing the specified fields. Given large result sets, When the request exceeds page size, Then the API supports pagination or asynchronous export with a job ID and provides a signed URL upon completion. Given export filters are applied, When provider, language, user, and success/failure flags are included, Then the exported dataset respects all filters exactly. Given multi-tenant isolation, When an admin requests export, Then only data for their org is included; cross-org access returns 403. Given rate limits, When more than the allowed number of exports are requested per hour, Then the API returns 429 with retry headers.
Domain Glossary and Non-Translatable Tokens
"As a resident, I want key HOA terms and unit numbers to remain accurate in translations so that there is no confusion about rules, money, or locations."
Description

Support a community-managed glossary to enforce consistent translations of HOA terminology (e.g., ARC, CC&R) and specify non-translatable tokens such as unit numbers, amounts, and dates. Inject glossary hints into provider requests when supported and apply post-processing rules to preserve protected spans. Provide an admin UI for glossary CRUD, import/export, and validation, and include runtime checks to ensure glossary coverage in translated content.

Acceptance Criteria
Preserve-As-Is Acronym Term: ARC
Given the community glossary contains the term "ARC" with rule preserve_as_is=true for all languages And the recipient's preferred language differs from the sender's language When translating the message "ARC will review lot 12-B on 03/15/2025" Then every occurrence of "ARC" in the output is exactly "ARC" And no translated variant of "ARC" appears in the output
Mapped Translation Term: Violation -> Infracción (es)
Given the community glossary defines the source term "violation" with target es="infracción", match=word_boundary, case_insensitive=true And the sender's language is English and the recipient's language is Spanish When translating the message "Second Violation Notice" Then the output contains exactly one occurrence of "infracción" And the output does not contain the word "violation"
Non-Translatable Tokens Preservation: Amounts, Units, Dates
Given non-translatable token rules are enabled for monetary amounts, unit identifiers, and dates in MM/DD/YYYY When translating the message "Pay $1,234.56 for Unit 12-B by 03/15/2025." Then the substrings "$1,234.56", "Unit 12-B", and "03/15/2025" appear unchanged in the output And the output contains no additional spaces or altered punctuation inside these tokens
Provider Glossary Injection and Fallback Post-Processing
Given Provider A supports glossary hint injection and Provider B does not And the glossary contains the term "CC&R" with rule preserve_as_is=true When translating via Provider A Then the request includes glossary hints containing "CC&R" And the output preserves "CC&R" unchanged When translating via Provider B Then no glossary hints are sent And post-processing preserves "CC&R" unchanged in the output
Admin UI: Glossary CRUD and Permissions
Given the user has the Community Admin role When they create a glossary entry with key="ARC", rule=preserve_as_is Then the entry is saved and immediately available to the translation runtime When they update the entry to rule=map_to target es="Comité ARC" Then subsequent English->Spanish translations apply "Comité ARC" When they delete the entry Then subsequent translations no longer apply the rule And an audit log entry is recorded for create, update, and delete actions Given a user without admin permissions When they attempt to access the glossary UI or APIs Then the system returns 403 Forbidden
Glossary Import/Export with Validation and Summary
Given a CSV import of 50 rows including 2 duplicate keys, 1 invalid regex, and 1 missing target When the admin uploads the file for validation Then the system reports errors with row numbers and reasons for 4 rows And allows importing the remaining 46 valid rows And displays summary: processed=50, imported=46, errors=4 When exporting the glossary as CSV and JSON Then both exports contain the same count of entries as in the system And field values match the stored records
Runtime Glossary Coverage Checks and Alerts
Given the glossary defines "violation" -> es="infracción" And a translation job produces output that still contains "violation" When the coverage check runs post-translation Then a warning is logged with message_id, term="violation", expected="infracción", provider, and timestamp And the Admin Dashboard increments the uncovered-terms counter for the last 24h And the message is queued for optional re-translation when an admin selects Reapply Glossary

Multilingual Nudges

Push, email, and SMS reminders go out in each recipient’s language, with smart fallbacks if preference isn’t set. Lifts open rates and on-time dues across diverse households.

Requirements

Language Preference Hierarchy & Fallbacks
"As a board admin, I want reminders to use each recipient’s preferred language with smart fallbacks so that messages are understandable and reach more residents without manual segmentation."
Description

Implement a robust language selection system that determines each recipient’s preferred language from explicit profile settings, household-level preferences, imported data, and inferred signals (e.g., last-open locale), then applies a configurable fallback chain (user -> household -> community default -> product default). Store preferences on the member profile, support bulk import via CSV/API, and allow admins to override per contact. At send time, resolve the language dynamically for each channel, ensure deterministic fallback when a translation is missing, and log the resolved locale for auditing. Handle multi-contact households and ensure idempotent updates. This integrates with the existing feed and billing events so any post-to-bill reminder uses the recipient’s resolved language automatically.

Acceptance Criteria
Resolve Language via Hierarchy at Send Time
Given a recipient with user-level preferred language "es-419" And the household preferred language is "fr" And the community default language is "en-US" And the product default language is "en" When the system resolves the recipient's language for a delivery Then the resolved locale is "es-419" And the resolution order is user -> household -> community default -> product default And if no value exists at any level, the resolution returns "en" (product default) And the resolution result is consistent across push, email, and SMS prior to per-channel template fallback
Deterministic Fallback When Channel Template Lacks Locale
Given a recipient whose resolved locale is "es-ES" And for SMS the "es-ES" template is unavailable but an "es" template exists And the configured translation fallback chain is exact match -> base language -> community default -> product default When composing the SMS message Then the system selects the "es" template And if the "es" template is unavailable, it selects the "en-US" template (community default) And if the "en-US" template is unavailable, it selects the "en" template (product default) And given identical inputs, repeated sends always select the same template
Audit Log Captures Resolved Locale and Fallback Path
Given a push, email, or SMS delivery is triggered for a recipient When the language is resolved and a template is selected Then an audit record is written containing: recipient ID, household ID (if any), community ID, channel, event ID, resolved locale, resolution source (user/household/community/product), selected template locale, fallback path (if any), timestamp (ISO-8601 UTC), and correlation ID And the audit record is retrievable by correlation ID and recipient ID via admin UI and API And the audit record is immutable once written
Bulk Import/API: Language Preference Upsert and Idempotency
Given a CSV file with columns member_id, household_id (optional), preferred_language, household_language (optional) using BCP 47 codes And some rows contain invalid or unsupported codes When the file is imported via UI or API Then rows with valid codes update the corresponding member and/or household language preferences And rows with invalid codes are rejected with per-row error details (row number, field, error code "INVALID_LOCALE") And re-importing the same file produces zero additional updates (idempotent) And partial failures do not prevent successful rows from being applied And imported changes appear on member profiles immediately after import completes
Admin Override Per Contact With Inheritance Control
Given an admin with permission "Manage Members" When the admin sets a contact's preferred language to "vi" Then the contact's profile stores "vi" as an explicit override And subsequent deliveries for that contact resolve to "vi" regardless of household/community defaults And when the admin clears the override to "Inherit", subsequent resolutions follow the hierarchy user -> household -> community -> product with no explicit user value And all changes are recorded with admin ID, timestamp, and old/new values in the audit log
Multi-Contact Household Language Resolution Independence
Given a household with two contacts: Alice (explicit "es") and Bob (no explicit) And the household preferred language is "fr" And the community default language is "en-US" When an announcement is sent to both contacts across push and email Then Alice's push and email content use "es" (subject to per-channel template fallback rules) And Bob's push and email content use "fr" And changing Alice's preference does not alter Bob's resolved language And each contact's resolved locale is logged separately
Post-to-Bill Reminders Use Latest Resolved Language
Given a post is converted to a bill and reminders are scheduled for recipients And a recipient changes their preferred language after scheduling but before send time When the reminder is sent Then the system resolves the language at send time using the hierarchy and uses that locale for content selection And if the selected locale's template is missing, the deterministic fallback chain is applied per channel And the resolved locale and fallback path are logged with the billing event's correlation ID
Localized Template & Translation Engine
"As a content manager, I want to maintain one set of event-driven templates with localized variants so that Duesly can auto-render accurate messages in each resident’s language."
Description

Create a centralized templating system that supports per-locale content for push, email, and SMS with variable interpolation, pluralization, and locale-aware formatting for dates, currency, and numbers. Provide translation management with versioning, glossary, and translation memory; integrate with machine translation (e.g., provider API) for first-pass drafts and allow human review/override. Ensure templates are keyed to product events (e.g., bill created, reminder, delinquency) and can render deterministically for any locale with safe fallbacks when a translation is missing. Store all translations with audit trails and enable rollback. This engine powers compose-once messaging that Duesly auto-localizes at send time.

Acceptance Criteria
Per-Locale Rendering for Push, Email, and SMS
Given a template containing variables, pluralization, and date/number/currency directives and a recipient with locale "es-MX" When the engine renders the template for push, email, and SMS channels Then variables are interpolated correctly, plural forms follow ICU rules for "es", and dates/numbers/currency are formatted per "es-MX" CLDR And the same inputs produce identical output across runs (deterministic render) And p95 single-template render latency is <= 50 ms per channel
Safe Fallbacks for Missing Translations
Given a template key missing in "es-MX" but present in "es" and "en-US" When rendering for a recipient with locale "es-MX" Then the engine selects the "es" translation; if "es" is also missing, selects "en-US"; if all are missing, emits the template key with placeholders And a missing_translation telemetry event is recorded with key, requested locale, and fallback used And the render completes without error and p95 additional latency due to fallback is <= 10 ms
Event-Keyed Template Selection and Preview
Given event "bill.created" with payload {amount: 125.50, currency: "USD", due_date: "2025-09-01", member_name: "A. Rivera", link: "https://example.com"} and locale "fr-FR" When rendering for email, SMS, and push Then the engine selects the active template version for event "bill.created" and channel, applies locale-aware formatting (e.g., 125,50 $US; 1 sept. 2025), and includes the link And the preview endpoint returns the exact output for "fr-FR" with timezone-aware date formatting using the recipient's timezone, falling back to community timezone then UTC And a stable content hash is produced for identical inputs to support idempotent sends
Translation Management: Versioning, Glossary, Memory, and Rollback
Given a translator with "Translations:Edit" permission updates key "reminder.subject" in locale "pt-BR" When saving the change Then a new version is created with author, timestamp, change notes, and diff from the prior version And glossary-enforced terms are preserved or flagged; save is blocked if a required term is violated unless overridden with justification And translation memory suggests at least 3 matches with similarity scores; accepted suggestions are attributed and stored And selecting Rollback restores a prior version and records an audit entry without deleting history
Machine Translation Drafts with Human Review
Given auto-MT is enabled and a new English source string is added When the MT job runs Then the system requests translations for configured locales via the provider API with variable placeholders preserved (e.g., {amount}) and no PII expanded And MT responses are stored tagged with provider, model, timestamp, and status "Draft" And a reviewer can Accept or Edit; upon approval, status changes to "Approved" and the MT draft is superseded And on provider error or timeout (>3s), the string is marked "Needs Translation" and a retry/backoff schedule is applied (max 5 attempts)
Channel Constraints, Escaping, and Safety
Given rendered outputs for email (HTML), SMS, and push When preparing for delivery Then email content is sanitized to an allowlist of tags/attributes; SMS and push outputs are plain text with variables escaped to prevent injection And messages respect channel limits: SMS segments per GSM-7/Unicode are calculated; if exceeding 2 segments, a configurable truncation with ellipsis is applied; push title <= 50 chars and body <= 200 chars; email subject <= 78 chars (RFC 5322) And secrets are not logged; all provider calls use TLS; rate limit MT calls to <= 10 RPS with exponential backoff on 429
Audit Trails and Observability
Given any create/update/delete of a translation, template, or configuration When the action is performed Then an immutable audit log entry is recorded with actor, action, entity, before/after, timestamp, IP, and request ID And dashboards expose metrics: translation coverage per locale (% non-fallback), fallback rate, render latency p50/p95/p99, MT success/error rates And alerts trigger when fallback rate exceeds 5% over 15 minutes or render p95 > 100 ms
Channel-Specific Message Formatting
"As a communications coordinator, I want messages to be automatically formatted for SMS, email, and push in each language so that delivery and readability are optimized without manual rework."
Description

Enforce channel-aware constraints and best practices per locale: SMS character set detection (GSM-7 vs Unicode), segmentation and concatenation handling, short-link insertion, and per-carrier limits; email HTML with RTL language support and accessible typography; and push payload size limits with localized fallbacks to concise summaries. Provide pre-send validation and previews per channel and locale, auto-truncate or reflow content safely without breaking variables, and surface warnings when content exceeds limits. Ensure consistent branding across channels while respecting each medium’s constraints to maximize deliverability and readability.

Acceptance Criteria
SMS Charset Detection, Segmentation, and Short-Link Handling
Given an SMS message in a specific locale containing only GSM-7 characters, When pre-send validation runs, Then the system detects GSM-7, calculates segments using 160 chars single / 153 chars per concatenated segment, and displays the segment count. Given an SMS message that includes any Unicode character (e.g., emoji or RTL script), When validation runs, Then the system detects Unicode, calculates segments using 70 chars single / 67 chars per concatenated segment, and displays the segment count. Given a URL is present and short-linking is enabled, When validation runs, Then the URL is replaced with a branded short link of length ≤ 22 characters and character/segment counts are recalculated accordingly. Given a per-campaign maximum of allowed segments (e.g., 3) is configured, When validation finds the message exceeds this limit in any locale, Then a blocking error is shown with the overage and suggested edits. Given carrier and region metadata is known, When message length risks truncation or filtering based on carrier-specific limits, Then a warning is surfaced listing affected carriers/regions. Given variables such as {{amount_due}} are present, When sample data is applied, Then character counts use resolved values and truncation never splits variable values or URLs.
Email RTL Support and Accessible Typography
Given an email for an RTL locale (e.g., ar, he, fa, ur), When previewing and sending, Then dir="rtl" is applied to html/body/container, content is right-aligned appropriately, and numbers/placeholders are isolated to render correctly (e.g., via <bdi> or equivalent techniques). Given any email template, When validation runs, Then base font size is ≥ 16px, line-height is between 1.4 and 1.6, and text color contrast ratio is ≥ 4.5:1 for body text. Given images and buttons are present, When rendering, Then images have alt text, buttons have visible focus states and a tappable area ≥ 44x44 px. Given semantic structure requirements, When accessibility checks run, Then header/main/footer landmarks exist and layout tables include appropriate roles/attributes for screen readers. Given previews across major clients (Gmail, Outlook, iOS Mail), When compatibility tests run, Then brand header/footer render consistently without clipped or overlapping content.
Push Payload Sizing and Localized Fallback Summaries
Given a localized push notification with title, body, and data payload, When validation runs, Then the estimated payload size per locale is calculated and displayed, and must be ≤ 4096 bytes for iOS and Android FCM. Given the payload exceeds the platform limit in any locale, When validation runs, Then the system automatically applies a concise fallback summary for that locale, preserving required keys and variables, and displays a warning. Given a fallback is applied, When previewing, Then the UI shows both the original and the fallback content for that locale and allows user override before send. Given deep link and custom data fields are present, When trimming is required, Then optional fields are removed before required keys, ensuring the notification remains functional.
Pre-Send Validation & Per-Channel, Per-Locale Previews
Given a campaign targets multiple locales and channels (SMS, Email, Push), When the user opens pre-send review, Then the system shows per-locale previews for each channel including SMS segment counts, push payload sizes, and email accessibility checks. Given any channel/locale violates constraints, When validation completes, Then warnings (non-blocking) and errors (blocking) are displayed with channel and locale labels and actionable remediation suggestions. Given the user switches the preview locale, When rendering, Then tokens resolve with locale-appropriate formatting (dates, currency, numbers) and directionality rules are applied. Given the user attempts to send with unresolved blocking errors, When Send is clicked, Then the send is blocked and the blocking errors are listed; sends with only warnings require explicit acknowledgment.
Safe Auto-Truncation and Reflow Preserving Variables
Given content exceeds channel limits, When auto-truncation is enabled, Then truncation occurs on word boundaries, does not split variables or URLs, and appends an ellipsis where appropriate. Given variables resolve to long values, When truncation or reflow occurs, Then the entire resolved variable is preserved; for SMS it may move into the next segment if within the configured max segments, otherwise a blocking error is raised. Given markup (e.g., simple HTML/Markdown) is present, When truncation/reflow occurs, Then tags remain balanced and dangling markup is removed to keep valid structure. Given required placeholders are unresolved in any locale, When validation runs, Then sending is blocked until sample data or locale-specific fallbacks are provided.
Consistent Branding Across Channels Under Constraints
Given brand theme assets (logo, colors, app name) are configured, When messages are rendered, Then SMS includes a compliant brand signature when within length limits, email includes branded header/footer, and push displays the correct app name and icon. Given adding an SMS signature would exceed the configured segment limit, When validation runs, Then the signature is omitted and a warning is logged indicating the reason. Given locale-specific font availability and reading direction, When rendering, Then locale-appropriate font stacks or fallbacks are used while maintaining brand color contrast thresholds. Given dark mode is detected in client/OS, When previewing, Then text and brand elements remain legible with invert-safe assets and no loss of contrast below 4.5:1.
Nudge Scheduling, Timezone & Quiet Hours
"As a board admin, I want reminders to send at appropriate local times with clear escalation rules so that residents are nudged effectively without being disturbed at night."
Description

Build a delivery orchestrator that schedules nudges relative to bill due dates and compliance milestones, resolves recipient timezones, and enforces community-configurable quiet hours per locale. Support retries, channel escalation (e.g., push → email → SMS) based on engagement or non-payment, deduplication across overlapping campaigns, and rate limiting. Provide idempotent send keys tied to feed/bill events, and integrate with the existing reminder automation so all sends are logged with timestamps, resolved locale, and channel outcome. Allow admins to set send windows and escalation rules globally or per campaign.

Acceptance Criteria
Locale Resolution and Template Fallback per Recipient
Given a recipient with preferred_language set to es-MX and channel templates available for es and es-MX, When a nudge is sent, Then the system selects es-MX templates and content for all channels. Given a recipient with no preferred_language but a household locale of vi-VN and available vi templates, When a nudge is sent, Then the system resolves locale to vi-VN and uses matching templates. Given a resolved locale that lacks a channel-specific template, When a nudge is sent, Then the system falls back to the base language (e.g., es from es-MX), and if unavailable falls back to en-US; if no fallback template exists, Then the send is skipped with outcome=skipped_missing_template. Given a campaign with mixed-channel templates, When a nudge is sent, Then all channels for a recipient use the same resolved locale in that send.
Timezone-Resolved Scheduling Relative to Due Dates and Milestones
Given a bill due date and time stored in UTC and a recipient with timezone America/Los_Angeles, When a rule specifies "7 days before at 09:00 local" and a send window of 08:00–20:00, Then the nudge is scheduled at 09:00 local 7 days prior in the recipient’s timezone. Given a computed send time outside the send window (before 08:00 or after 20:00), When scheduling occurs, Then the send is adjusted to the next available time within the window (if after window end, schedule at next day’s window start). Given a recipient without a stored timezone, When scheduling, Then timezone is resolved in order: user setting → household address-derived tz → community default tz; scheduling uses the resolved tz. Given a compliance milestone trigger with offset "+3 days at 18:00 local", When scheduling for recipients in different timezones, Then each recipient’s send time is calculated independently in their local time.
Quiet Hours Enforcement by Locale with Daylight Saving Handling
Given a community-configured quiet hours window of 21:00–08:00 for locale en-US, When a send would occur at 22:15 local, Then the send is deferred to 08:00 local next permissible day. Given a locale observing DST where the clock moves forward at 02:00, When quiet hours span 21:00–08:00, Then no sends are dispatched during the skipped hour and the 08:00 wall time is honored on the new offset. Given overlapping quiet hours and a campaign-specific send window, When both apply, Then the intersection of allowed times is used and no send occurs outside the intersection. Given a send deferred due to quiet hours, When the next allowed window opens, Then the deferred send is dispatched before new lower-priority sends for the same recipient/campaign.
Channel Escalation and Retry with Stop Conditions
Given an escalation ladder Push → Email → SMS with delays of 12h and 24h, When a Push nudge is neither delivered nor opened within 12h, Then Email is sent; if Email is unopened for 24h, Then SMS is sent. Given any engagement (open, click) or payment recorded for the targeted bill/compliance item, When evaluating escalation, Then all future escalations and retries for that item-recipient are canceled. Given a transient delivery error (e.g., 5xx from provider), When sending a channel step, Then retries occur with exponential backoff (e.g., 5m, 30m, 2h) up to the configured max_attempts; hard bounces or invalid numbers are not retried and mark the channel as undeliverable. Given a channel step succeeds (delivered), When the recipient later engages, Then subsequent scheduled escalations for that step are canceled.
Deduplication Across Overlapping Campaigns Using Idempotent Keys
Given multiple campaigns reference the same bill event for the same recipient and channel, When sends carry the same idempotency key (event_id+recipient_id+channel), Then exactly one send is created and transmitted and others are skipped with outcome=skipped_duplicate. Given the scheduler is re-run or retried for the same idempotency key, When it attempts to enqueue, Then no additional send is enqueued or dispatched. Given a suppression window of 24h for cross-campaign dedup, When two different campaigns target the same recipient and bill event within 24h on the same channel, Then only the first send is dispatched; the second is skipped with outcome=skipped_suppressed. Given message content is updated but the idempotency key is unchanged, When a resend is attempted, Then the send is not reissued and the attempt is logged as skipped_duplicate.
Rate Limiting and Throttling Across Recipients and Channels
Given per-recipient caps of 3 total nudges/24h and 1 SMS/24h, When scheduling multiple sends, Then no recipient receives more than 3 nudges across channels nor more than 1 SMS within any rolling 24-hour window. Given provider throughput limits (e.g., 600 emails/min, 30 SMS/sec), When bulk sends are dispatched, Then the system queues and spaces messages to not exceed configured limits while preserving relative send order by priority and scheduled time. Given a campaign-level hourly cap (e.g., 5,000 sends/hour), When the cap is reached, Then remaining sends are deferred to the next hour respecting quiet hours and send windows. Given a send is deferred by rate limiting, When it is rescheduled, Then it remains within its escalation SLA (do not exceed maximum allowed delay); if SLA would be exceeded, Then the send is canceled with outcome=skipped_sla_violation.
Audit Logging and Observability of Nudge Orchestration
Given any send attempt or outcome, When the event occurs, Then a log record is written within 60 seconds containing: UTC timestamp, local timestamp, campaign_id, recipient_id, channel, resolved_locale, resolved_timezone, idempotency_key, trigger_type, template_version, escalation_step, provider_message_id (if applicable), outcome status, retry_count, and error_code/message (on failure). Given a payment is posted for a bill targeted by a nudge, When correlating events, Then the payment record links to the most recent related nudge and the nudge timeline shows the payment event. Given an admin filters the send log by campaign, recipient, channel, or outcome, When the query is executed, Then matching records are returned within 2 seconds for datasets up to 100k records. Given an API client requests the send log for a given idempotency_key, When the request is made, Then exactly one record per channel step is returned with its final outcome and all intermediate retry attempts accessible via an attempts sub-collection.
Opt-in/Opt-out and Compliance per Locale
"As a resident, I want to easily manage my notification preferences in my language so that I only receive messages I’ve consented to and understand how to opt out."
Description

Implement consent capture and enforcement for email and SMS with localized consent copy, double opt-in where required, and per-channel opt-out that is honored across all campaigns. Support STOP/UNSUBSCRIBE/HELP keywords in multiple languages, localized legal footers, jurisdiction-specific quiet hours, and data retention rules. Maintain immutable audit logs of consent state changes and message history, and expose a localized self-service preferences page. Integrate compliance checks into the send pipeline to block sends when consent is missing or revoked.

Acceptance Criteria
Localized Consent Capture & Double Opt-In
Given a recipient’s locale requires double opt-in, When the recipient submits an opt-in via email or SMS, Then a localized confirmation request is sent in that channel within the configured SLA Given a confirmation request was sent, When the recipient confirms within the configured confirmation window, Then the channel consent becomes Active and the system records locale, timestamp, consent text version, and confirmation method Given a confirmation request was sent, When the recipient does not confirm within the configured window, Then the consent remains Inactive and the recipient does not receive campaign messages on that channel Given a locale does not require double opt-in, When a recipient opts in, Then the channel consent becomes Active immediately and locale, timestamp, and consent text version are recorded Given a recipient has no language preference, When consent copy is displayed or sent, Then the system applies the configured language fallback and records the language used
Per-Channel Opt-Out via Multilingual Keywords
Given a recipient sends a recognized STOP/UNSUBSCRIBE keyword in any supported language via SMS, When the message is received, Then SMS consent is set to Revoked and a single localized confirmation of opt-out is returned Given SMS consent is Revoked, When any campaign attempts to send an SMS to that recipient, Then the send is blocked and a structured reason "No consent - SMS revoked" is logged Given a recipient replies HELP in a supported language via SMS, When the message is received, Then a localized HELP response is sent including support contact and opt-out instructions Given a recipient clicks an email unsubscribe link, When the request is processed, Then email consent is set to Revoked while SMS consent remains unchanged Given keywords include accents or case variations, When processed, Then they are matched case- and accent-insensitively and the detected language is logged
Send Pipeline Compliance Gate
Given a message is queued for delivery, When the compliance gate runs, Then it verifies channel consent status, locale-specific double opt-in requirements, legal footer availability, and quiet hours before authorizing send Given channel consent is missing or revoked, When the compliance gate evaluates the message, Then delivery is blocked, no message is sent, and a machine-readable error code and reason are logged Given quiet hours apply in the recipient’s jurisdiction, When evaluated, Then the message is deferred to the next permissible window and the new scheduled time is recorded Given the message is authorized, When rendering completes, Then the locale-specific legal footer and sender identity are appended and the final rendered artifact is stored with locale and template/legal footer version identifiers
Jurisdiction-Specific Quiet Hours Enforcement
Given a recipient’s jurisdiction defines quiet hours, When scheduling or sending SMS or calls, Then deliveries occurring within quiet hours (recipient local time) are suppressed Given a delivery is suppressed by quiet hours, When rescheduling occurs, Then the message is queued for the earliest permissible time with up to the configured jitter and the suppression/reschedule event is logged Given a jurisdiction has no quiet hours, When scheduling, Then no suppression is applied for quiet hours Given an admin attempts to manually override, When the target time falls within quiet hours, Then the override is rejected and the applicable policy is displayed
Immutable Audit Trail for Consent and Messaging
Given any consent state change occurs, When the change is committed, Then an append-only log entry is written with actor, channel, old/new state, locale, consent text version, timestamp (UTC), and source (API/UI/keyword) Given a message is sent, blocked, deferred, or failed, When the event occurs, Then an immutable log entry captures event type, channel, recipient ID, locale, template ID/version, legal footer ID, reason/error code, and correlation IDs Given an attempt is made to alter or delete an audit entry, When processed, Then the system prevents mutation and records a security event Given an auditor requests a recipient’s history, When export is generated, Then the system produces a chronological, signed export with integrity proof (hash chain/checksum)
Localized Self-Service Preferences Page
Given a recipient opens the preferences page, When locale is determined from profile, URL, or browser settings, Then the page renders in that language with localized consent copy and legal disclosures Given the recipient opts in for a channel in a locale requiring double opt-in, When the change is saved, Then a confirmation workflow starts and the UI shows Pending until confirmation; in other locales, the status becomes Active immediately Given the recipient opts out of a channel, When the change is saved, Then the opt-out takes effect immediately, is reflected in the UI, and a confirmation message is sent in the selected language Given accessibility requirements, When navigating the page, Then it meets WCAG 2.1 AA for language attributes, labels, and screen reader announcements for consent status updates Given a recipient accesses the page via a secure tokenized link, When unauthenticated, Then they can view and modify preferences for that identity only and all changes are audit logged
Locale-Based Data Retention and Deletion
Given locale-specific data retention rules are configured, When evaluating retention, Then message content and delivery metadata are retained per channel and locale for their configured durations, while consent audit logs are retained per legal minimums Given a record reaches end of retention, When the purge job runs, Then the record is irreversibly deleted or redacted within the configured SLA and a deletion log entry records record type, count, locale, and timestamp Given a legal hold is applied to a subject or time range, When the purge job runs, Then affected records are skipped and the hold reason and expiration are logged Given an export or search requests a deleted record, When processed post-purge, Then the system returns a placeholder "Deleted per retention policy" without exposing content
Localization-aware Analytics & Conversion Tracking
"As a product owner, I want to see engagement and payment lift by language and channel so that we can optimize copy and investment where it matters most."
Description

Provide dashboards and exports that break down open, click, and payment conversion rates by language, channel, campaign, and community. Attribute payments to prior nudges with a configurable lookback window, and support per-locale A/B tests to compare copy variants. Surface insights such as languages with low engagement or high delinquency improvement to guide future content and translation investment. Ensure metrics are privacy-aware and align with consent settings.

Acceptance Criteria
Dashboard Breakdown by Language, Channel, Campaign, and Community
Given messages have been sent across multiple languages, channels, campaigns, and communities within a selectable date range When a user opens the Analytics dashboard and applies filters for date range, community, campaign, channel, and language (including a toggle to include fallback-sent messages) Then the dashboard displays sends, opens, open rate, clicks, click rate, payments, and payment conversion rate grouped by the selected dimensions, and totals reflect the filtered subset And each metric matches event-store ground truth within ±0.1% for rates and exact counts for sends/opens/clicks/payments And selecting/deselecting any filter updates all widgets and tables within 1 second for cached queries and 5 seconds for cold queries And segments with zero activity are hidden by default and can be shown when "Include zero-activity segments" is enabled
Configurable Lookback Attribution for Payments
Given a payment occurs after one or more nudges to the same recipient When the attribution model is set to Last Touch and the lookback window is configured to 7 days Then the payment is attributed to the most recent nudge prior to the payment timestamp within 7 days; otherwise it is marked Unattributed And changing the lookback window to 30 days recomputes attributed counts consistently across dashboards and exports within 10 minutes And attribution respects channel and campaign filters and is computed per recipient And the applied attribution model and lookback window are visibly indicated on the dashboard and embedded in exports
Per-Locale A/B Testing and Variant Analytics
Given a campaign in locale es-ES defines two copy variants A and B with a 50/50 allocation When the campaign sends nudges via email, push, and SMS Then recipients in es-ES are randomly assigned to A or B within the locale and remain sticky to their assigned variant across channels for that campaign And the dashboard reports sends, opens, clicks, payments, and conversion rate per variant within es-ES and overall for the locale And the system computes and displays statistical significance (two-tailed z-test) for payment conversion difference when sample sizes >= 1,000 per variant, otherwise shows "Insufficient sample" And exports include locale, variant, allocation, and metrics per variant
Privacy-Aware Metrics Respecting Consent
Given a community-level setting "Analytics Consent Required" is enabled and some recipients have opted out of tracking When nudges are sent to both consenting and non-consenting recipients Then individual opens/clicks are recorded only for consenting recipients; non-consenting recipients contribute only to aggregate send counts And per-recipient logs and exports exclude non-consenting recipients entirely And aggregate dashboards indicate the percentage of traffic excluded due to consent and base rates on consenting populations And processing of consent changes is applied prospectively within 15 minutes and is auditable
Analytics Exports with Attribution Metadata
Given a user requests a CSV export for a date range, community, and campaign filters When the export is generated Then the file contains one row per combination of date (in the account timezone), community_id, campaign_id, channel, language, and variant with columns: sends, opens, open_rate, clicks, click_rate, payments, payment_conv_rate, attributed_payments, attribution_model, lookback_window_days And numeric totals match the dashboard for the same filters And the export generation completes within 2 minutes for up to 1,000,000 aggregated rows and streams for larger datasets And the file includes a header row, ISO-8601 timestamps, UTF-8 encoding, and a data dictionary link
Insights for Low Engagement and Delinquency Improvement
Given at least 4 languages have activity in the selected community during the last 30 days When the Insights panel is opened for that community and period Then the system flags languages whose open rate is ≥20% below the community median open rate and languages whose on-time payment rate improved by ≥10% versus the prior 30-day period And each insight lists the affected language, metric deltas, sample sizes, and top channels contributing And clicking an insight applies the corresponding filters to the dashboard And insights are refreshed daily by 06:00 local time
Admin Preview, QA & Overrides
"As a board admin, I want to preview and adjust localized messages across channels before they go out so that quality stays high and errors are caught early."
Description

Add an admin workspace to preview any message in multiple languages and channels before sending, including real data substitution, SMS length counters, and push payload validation. Allow inline translation edits, per-locale attachment substitutions, and the ability to send test messages to target devices/emails. Provide warnings for missing translations with one-click fallbacks and a workflow to request or assign translation updates. Respect role-based permissions for who can edit templates and approve changes.

Acceptance Criteria
Channel Preview with Real Data, SMS Segments, and Push Validation
Given I have Template Editor permission and open Admin Preview for template {templateId} And I select recipient {recipientId} and locale {locale} When I switch between channels Email, SMS, and Push Then the preview renders with all template variables substituted from the selected recipient, account, and community records And unknown or unresolvable variables are highlighted and listed as errors And the SMS preview shows GSM-7 vs Unicode detection, character count, and segment count using 160/153 (GSM-7) or 70/67 (UCS-2) rules And the SMS preview flags messages exceeding 10 segments with a warning And the push preview validates payload schema (title, body, data) and size ≤ 4096 bytes and surfaces validation errors inline And the preview refreshes within 500 ms when changing locale or channel
Inline Translation Editing and Per-Locale Attachments
Given I have Template Editor permission When I select locale {locale} and click Edit Then I can edit subject, body, and push title/body for that locale And saving creates/updates a locale-specific Draft without altering other locales And I can attach or replace files for the selected locale only And attachment validation enforces allowed types (PDF, PNG, JPG) and max size 10 MB per file And placeholders are validated; unknown tokens are blocked from save with a list of missing variables And the localized preview displays the updated text and attachments immediately after save
Missing Translation Warning, One-Click Fallback, and Assignment
Given a template has untranslated fields in one or more targeted locales When I open Admin Preview or the pre-send QA view Then a warning lists the missing locales and fields And a One-Click Fallback action applies default-locale content for those fields and marks them as Using Fallback And I can create a Translation Request by assigning a user or group with optional due date and notes And the request is recorded and visible with status (Open, In Progress, Done) And send approval is blocked until each targeted locale has localized text or an explicit fallback applied
Send Test Messages to Target Endpoints with Delivery Logs
Given I have permission to send tests When I choose a locale and channel and enter a target email/phone/device token or select a stored test device And I click Send Test Then the system sends a test message with [TEST] prefix and a unique test identifier And delivery events (queued, sent, delivered, failed/bounced) are captured and visible in the activity log within 60 seconds And test sends are rate-limited to 3 per template per channel per user per hour; additional attempts are blocked with an explanatory message And test sends do not affect production analytics or trigger resident reminders
Role-Based Edit and Approve Permissions Enforcement
Given role-based permissions are configured When a user without Template Editor permission opens the Admin Preview Then edit controls are disabled/hidden and write API attempts return 403 Forbidden When a user with Template Editor edits a template Then the change is saved as Draft and requires approval before publish When a user with Approver (who is not the change author) reviews a Draft Then they can Approve or Reject with a required comment And all approvals/rejections are timestamped and attributed to the approver
Versioning, Draft Workflow, and Audit Trail
Given a template has existing versions When I save changes Then a new Draft version V+1 is created with per-locale diffs recorded And I can view side-by-side diffs of text and attachments between any two versions And I can revert to any prior Published version with a single action And publishing a Draft promotes it to Published and archives the prior Published version And all actions (edit, fallback applied, attachment change, approve/reject, publish) are written to an immutable audit log with user, timestamp, and IP
Language Preference Fallback Simulation and Override
Given a recipient has no language preference set When I open Admin Preview and select Simulate Fallback Then the system shows the fallback chain (Recipient → Household → Community Default → English) and the locale that will be used And I can override the fallback locale for this send preview to any available locale And the preview updates across Email, SMS, and Push to reflect the chosen fallback/override And the override must be explicitly confirmed before send approval can proceed

Translation Ledger

Logs the when/what/how of every translation—engine, glossary hits, user toggles—linked to the post or bill. Creates a defensible audit trail for compliance and charge disputes.

Requirements

Immutable Translation Event Ledger
"As a compliance officer, I want a complete, tamper-evident record of translation activity for each post or bill so that I can prove what was shown and when during audits or disputes."
Description

Implement an append-only, tamper-evident ledger that records every translation event across posts, bills, comments, and outbound notifications. Each record must capture source and target language, event type (auto-translate, manual edit, user view, resend), timestamps (requested, completed), content fingerprints (hash of source and rendered translation), related object IDs (post/bill/notification), actor identifiers (system, user), trigger context (UI action, API, automation), and environment metadata (tenant, region). The ledger must chain entries with cryptographic hashes for integrity, encrypt at rest, partition by tenant for multi-tenant isolation, and expose query APIs keyed by object ID and time range. Retention and purge policies must be configurable per community while preserving hash chain continuity and auditability.

Acceptance Criteria
Append-Only Tamper-Evident Hash Chaining
Given a ledger with N entries When a new translation event is recorded Then a new entry is appended with prev_hash equal to the last entry’s hash and the ledger length becomes N+1 Given any request to update or delete an existing ledger entry When the API or storage layer processes it Then the operation is rejected with 403 Forbidden and no data is modified Given the ledger entries 1..k When the hash chain is recomputed using the configured hashing algorithm Then each entry’s stored hash equals the recomputed hash and any tampering produces a detectable mismatch at the first altered link Given concurrent writes for the same object When entries are appended Then the resulting chain remains linear and ordered by monotonic sequence IDs with correct prev_hash linkage and no orphaned entries
Complete Translation Event Record Fields
Given an auto-translate of a post from en to es When the ledger entry is created Then the entry includes non-null fields: source_lang=en, target_lang=es, event_type=auto_translate, timestamps.requested (UTC ISO 8601), timestamps.completed (UTC ISO 8601), content_hash.source, content_hash.rendered, object_type=post, object_id=<post_id>, actor_type=system, actor_id=system, trigger_context=automation|API|UI, tenant_id, region Given a manual edit of a translation by user U on a bill When the ledger entry is created Then event_type=manual_edit, object_type=bill, actor_type=user, actor_id=U, and content_hash.rendered represents the edited translation Given a user toggles view original/translated on a comment When the ledger entry is created Then event_type=user_view, object_type=comment, content_hash.source and content_hash.rendered reflect the currently displayed versions, and timestamps.requested/completed are populated Given a resend of an outbound notification translation When the ledger entry is created Then event_type=resend, object_type=notification, object_id=<notification_id>, and source_lang/target_lang are set to the languages used
Multi-Tenant Partitioning and Isolation
Given tenants A and B When translation events are written for both tenants Then entries are stored in tenant-scoped partitions keyed by tenant_id and contain the correct tenant_id and region metadata Given an API client authenticated for tenant A When querying any ledger endpoint by object_id belonging to tenant B Then the request is denied with 403 or returns 404 without leaking existence, and no entries from tenant B are returned Given a cross-tenant scan attempt without explicit multi-tenant admin authorization When executed Then zero rows from other tenants are returned (row_count=0) and the access is logged
Encryption At Rest for Ledger Storage
Given ledger data persisted to storage When inspecting the raw storage artifacts Then event payloads and hashes are encrypted at rest using the configured KMS-managed keys and cannot be read without valid decryption context Given a key rotation event When new translation events are written and old entries are read Then new entries are encrypted with the new key version and existing entries remain decryptable, with no data loss or integrity failures Given a storage snapshot exfiltrated without keys When attempting to read ledger entries Then plaintext fields and content hashes are not recoverable
Query APIs by Object ID and Time Range
Given object_id=X and time range [t1, t2] When GET /translation-ledger?object_id=X&from=t1&to=t2 is called Then the response contains only entries for X with requested_timestamp in [t1, t2], sorted ascending by requested_timestamp Given more than page_size entries match When the API is called without a cursor Then it returns page_size entries and a continuation cursor; when called with the cursor, the next page is returned with no duplicates or gaps Given a nonexistent object_id When queried over any time range Then the API returns 200 with an empty result set
Retention and Purge with Chain Continuity and Auditability
Given community C has retention_days=365 configured When the scheduled purge job runs Then ledger entries for C older than 365 days are removed from active storage and the action is logged with counts Given a purge boundary is crossed When the integrity verification endpoint is called for a range spanning purged data Then it returns a verifiable proof that the current head hash links to an anchor hash representing the pre-purge segment, preserving end-to-end chain continuity Given an audit request for object_id=X over [t1, t2] where some entries were purged When the verification is performed Then the API returns proofs and metadata sufficient to demonstrate no tampering across the purged interval
Coverage of Posts, Bills, Comments, and Outbound Notifications
Given translation-related actions on posts, bills, comments, and outbound notifications When auto-translate, manual edit, user view toggle, or resend occurs Then a ledger entry is created for each action with correct object_type and object_id linkage Given any outbound notification resend in a target language When triggered by automation or UI Then a resend event is logged with linkage to the original notification and the applicable source_lang and target_lang values Given a user toggles translation visibility in the UI for any supported object When the view changes Then a user_view event is logged with actor_id, actor_type=user, and timestamps.requested/completed populated
Engine and Model Version Attribution
"As a product support agent, I want to see the exact engine and model settings used for a translation so that I can explain output differences and resolve customer questions."
Description

Capture and persist attribution for the translation engine and configuration used on every event, including provider name, model/version, configuration parameters (domain, formality, glossary usage, fallback path), provider request/trace IDs, latency, and confidence/quality scores when available. When failover occurs, record the ordered chain of providers/models used and the reason for failover. Surface this attribution in the ledger APIs and UI to enable side-by-side comparison across versions and to explain divergences in output over time.

Acceptance Criteria
Persist attribution on successful translation
Given a translation is executed for a post or bill using a specified provider, model, and configuration (domain, formality, glossary usage) And the provider returns a response (with optional request/trace IDs and confidence/quality scores) When the translation completes successfully Then the translation event record persists provider_name, model, model_version, domain, formality, glossary_used (boolean), glossary_id (when used), and fallback_path containing one entry for the used provider/model And persists provider_request_id and provider_trace_id when supplied by the provider And records latency_ms for the provider call And links the event to the originating post/bill ID and language pair (source_locale, target_locale)
Record ordered failover chain with reasons
Given the primary translation attempt fails or is rejected due to policy thresholds When the system falls back to one or more secondary providers/models Then the event stores ordered_failover_chain with 1..N steps each containing provider_name, model, model_version, reason (enum: Timeout, 4xx, 5xx, NetworkError, RateLimited, QualityThresholdNotMet, Unknown), start_ts, and end_ts And the final step matches the provider/model that produced the output (or the event is marked failed with no output) And fallback_path reflects the exact ordered list of providers/models attempted
Persist per-attempt request/trace IDs and latency
Given one or more provider calls are made for a translation (including failed attempts) And the provider returns request_id and/or trace_id values When the attempt completes (success or failure) Then each attempt entry records provider_request_id and provider_trace_id for that attempt And each attempt entry records latency_ms computed from attempt start and end timestamps And missing IDs are stored as null without causing persistence or API errors
Surface attribution fields in Ledger API
Given translation events exist with attribution data When a client requests the Translation Ledger API for a specific post or bill Then each event in the response includes provider_name, model, model_version, domain, formality, glossary_used, glossary_id (when used), fallback_path, ordered_failover_chain, provider_request_id, provider_trace_id, latency_ms, confidence_score, quality_score (when available), source_locale, target_locale, and created_at And fields that are not applicable are present with null values rather than omitted And the API returns the events associated to the requested entity_id
Display side-by-side comparison in UI
Given a user views the Translation Ledger for a post or bill that has at least two translation events for the same target locale When the user selects Compare Then the UI renders a side-by-side view showing for each event: provider_name, model, model_version, domain, formality, glossary_used and glossary_id, fallback_path summary, latency_ms, confidence/quality scores (if available), and translation timestamp And differences in these fields between the two events are visually highlighted And an explanation panel lists which fields changed between versions to explain output divergences
Link events to entities and language pairs
Given a translation is triggered from a post or bill When the translation event is created Then the event stores entity_type (post or bill), entity_id, source_locale, and target_locale And writes must validate that entity_id references an existing post or bill; otherwise the write fails with a 4xx error And the Ledger API returns the event when filtered by that entity_id
Glossary and TM Hit Logging
"As a community manager, I want to verify which glossary terms and TM entries were applied to a translated notice so that I can demonstrate that mandated phrasing was honored."
Description

Log every applied glossary term and translation memory (TM) match used during translation, including glossary ID/version, term/phrase matched, match type (exact/forced/fuzzy), before/after segment text, and TM entry IDs with match percentages. Summarize counts per object (e.g., bill) and expose detailed hit lists via API and UI. Preserve historical context so changes to glossaries do not rewrite past records. Provide indicators in the audit view highlighting where compliance-critical terms were enforced.

Acceptance Criteria
Log Glossary Term Hit Details on Translation
Given a post or bill is translated into a target language with glossary enforcement enabled When the translation completes and segments are produced Then for each segment containing a matched glossary term, the system creates a glossary hit record with fields: object_id, object_type, locale_source, locale_target, translation_version_id, segment_id, glossary_id, glossary_version, term, match_type in [exact, forced, fuzzy], source_segment_snapshot (before), target_segment_snapshot (after), enforcement_flag (true/false), engine_id, user_id (if human edit applied), created_at And each glossary hit record has a stable unique ID and is immutable after creation And the hit record links to the parent object and translation version for retrieval
Log Translation Memory Match Details
Given translation memory (TM) is enabled for the workspace When a segment is translated and a TM suggestion is applied to produce the target segment Then the system creates a TM hit record with fields: object_id, object_type, locale_source, locale_target, translation_version_id, segment_id, tm_entry_id, match_percentage (0–100), match_type in [exact, fuzzy], source_segment_snapshot, target_segment_snapshot, engine_id, created_at And only the applied TM match is logged per segment (selected=true), not merely consulted candidates And each TM hit record has a stable unique ID, is immutable, and links to the parent object and translation version
Summarize Hit Counts per Object and Version
Given an object has completed a translation run resulting in glossary and/or TM hits When the translation version is finalized (saved) Then the system computes and stores a summary per object_id + translation_version_id with fields: glossary_hits_total, glossary_hits_exact, glossary_hits_forced, glossary_hits_fuzzy, tm_hits_total, tm_hits_100, tm_hits_95_99, tm_hits_85_94, tm_hits_lt_85, unique_glossary_terms_count, compliance_critical_terms_count And the summary is retrievable via API and visible in the UI audit header for that version And creating a new translation version generates a new summary without altering prior version summaries
Expose Detailed Hit Lists via API
Given a client with read permission requests translation hit details When it calls GET /objects/{object_id}/translations/{version_id}/hits with optional filters (type in [glossary, tm], match_type, term, tm_entry_id, compliance_critical, segment_id) and pagination (page, page_size) Then the API responds 200 with a paginated list including required fields for each hit and stable IDs, plus total_count and pagination cursors/links And requests without sufficient permission return 403; nonexistent object/version return 404; invalid parameters return 400 And for datasets up to 1,000 hits, median response time is <= 500 ms under test conditions
UI Audit View Highlights Compliance-Critical Terms
Given a user opens the Translation Ledger audit view for a specific object and translation version When compliance-critical glossary terms were enforced in the translation Then the UI presents in-line highlights within the content preview and a side-panel list with a visible "Enforced" badge for each instance And selecting a highlight scrolls to the segment and opens a details drawer showing the associated hit record fields And a toggle filter "Show only compliance-critical" updates the list and counts immediately without page reload
Preserve Historical Context Across Glossary Changes
Given glossary version V produced stored glossary hit records for an object/version When the glossary is later edited to version V+1 or terms are deleted/modified Then previously stored hit records and summaries continue to reference glossary_id and glossary_version V and retain their original term and segment snapshots And API/UI retrieval returns the original stored data rather than recomputing from the current glossary And attempts to modify historical hit records via API are rejected with an error (405 or 403)
User Language Toggle History
"As a billing specialist, I want to know which language a payer viewed at the time of payment so that I can resolve charge disputes with accurate context."
Description

Track and record user-facing interactions related to language, including language selection/toggles, auto-translate on/off, and the timestamp and context (which post/bill) when a user viewed content in a given language. Store minimal identifiers (user ID, role) and device/channel when available, with rate limits to avoid excessive logging. Respect user privacy preferences and provide redaction workflows that preserve event integrity while masking personal data. Expose per-user, per-object view histories in the audit trail.

Acceptance Criteria
Log language toggle on a post
Given an authenticated user with role R is viewing post P in language L1 When the user selects language L2 from the language toggle Then a language_toggle event is written within 500 ms containing: userId, role, objectType=post, objectId=P, fromLanguage=L1, toLanguage=L2, autoTranslateEnabled (true|false), timestamp (RFC 3339 UTC, ms), device (ios|android|web|unknown), channel (app|web|email|push|unknown) when available, requestId And the event is linked to post P in the Translation Ledger And no duplicate event is stored if the same toggle occurs again within 1 second for the same userId/objectId/fromLanguage/toLanguage
Log auto-translate toggle on a bill
Given an authenticated user with role R is viewing bill B When the user toggles Auto-Translate to a new state S (on|off) Then an auto_translate_toggle event is written within 500 ms containing: userId, role, objectType=bill, objectId=B, newState=S, activeLanguage=Lx, timestamp (RFC 3339 UTC, ms), device, channel And no duplicate event is stored if the same state S is re-applied within 1 second for the same userId/objectId And the event appears in the per-user, per-object audit endpoint within 2 seconds of the action
Record view-in-language event on first render
Given a user views content object O (post or bill) rendered in language Lx When the content becomes visible to the user Then a view_language event is recorded once per userId/objectId/language within a rolling 5-minute window, with: userId, role, objectType, objectId, language=Lx, timestamp (RFC 3339 UTC, ms), device, channel And the event is not recorded again within the 5-minute window even if the user re-opens the same object in Lx And switching to a different language within the window creates a new view_language event for that new language
Normalize and store device and channel context
Given an event is captured with context that includes device and channel When the context is processed Then device is normalized to one of [ios, android, web, unknown] and channel to one of [app, web, email, push, unknown] And if device/channel are unavailable they are stored as unknown, not empty or null And no IP address, precise location, or raw user-agent string is stored in the event payload
Respect privacy preferences and support redaction
Given a user has enabled a privacy preference doNotTrack=true When the user performs language-related actions Then no personal identifiers are persisted; userId is stored as null and role is retained; events still include object references and timestamps And the per-user audit endpoint returns 403 with reason=privacy_opt_out for that user Given a redaction request is submitted for userId U When the redaction job completes (within 24 hours) Then all existing events with userId=U have personal fields (userId, deviceId, IP, userAgent) removed or replaced with non-reversible placeholders, while preserving eventId, object references, timestamps, and counts And a redaction_audit event referencing the jobId is appended to the ledger
Expose per-user, per-object audit trail
Given an authorized auditor requests GET /audit/users/{userId}/objects/{objectId}?types=language_toggle,auto_translate_toggle,view_language When events exist for the pair Then the API returns 200 with a chronological list (newest last) of events including: eventType, timestamp, fromLanguage, toLanguage, language, autoTranslateEnabled/newState, device, channel, objectType, objectId And results are paginated with default pageSize=50 and max=500, and the first page is returned within 2 seconds for up to 1,000 events total And unauthorized callers receive 403, and requests for non-existent pairs return 404
Apply rate limits and deduplication
Given a user generates language-related events for the same object When events exceed the following thresholds Then language_toggle and auto_translate_toggle are deduplicated with a 1-second minimum spacing per userId/objectId/eventType/value And view_language is limited to one per userId/objectId/language per 5 minutes And a per-userId per-objectId cap of 100 translation-ledger events per hour is enforced; events above the cap are not persisted and a counter metric is emitted
Audit Trail Viewer and Export
"As a board secretary, I want to filter and export a translation audit trail for a specific bill so that I can provide verifiable evidence to our payment processor."
Description

Provide a role-based UI within Duesly to view the translation ledger per post, bill, or notification with filters by time, language, event type, engine, and user. Include diff views of source vs. translated segments, glossary hit highlights, and integrity indicators (hash chain status). Enable exports to PDF, CSV, and JSON, embedding signatures, hash summaries, and timestamps. Offer an API endpoint to programmatically retrieve the same data. Apply access controls to restrict sensitive fields and support redaction and watermarking on exports.

Acceptance Criteria
Role-Based Ledger Access and Sensitive Field Restrictions
- Given an unauthenticated user, when they request the Audit Trail Viewer, then the system returns 401 and no ledger data is rendered. - Given an authenticated user without the Ledger:View permission, when they attempt to open any translation ledger, then the system returns 403 and no ledger data is rendered. - Given a user with Ledger:View, when they open the ledger for a post/bill/notification they have object access to, then the ledger entries display and are limited to that object. - Given a user without Ledger:ViewSensitive, when sensitive fields are present, then those fields are redacted in UI and in exports; attempts to request unredacted exports return 403. - Given a user with Ledger:ViewSensitive, when they toggle Redaction On/Off in the viewer, then sensitive fields mask/unmask accordingly and the current redaction state is applied to any export generated in that session. - Given any user lacking Ledger:ExportUnwatermarked, when they generate an export, then a watermark is applied to PDF and a watermarked:true flag is included in JSON metadata.
Ledger Filtering by Time, Language, Event Type, Engine, and User
- Given a ledger containing entries across multiple times, languages, event types, engines, and users, when a single filter is applied (e.g., Language=es), then only entries matching that filter are shown and the total count reflects the filtered set. - Given multiple filters are applied, when filters are combined (e.g., Time=Last 30 days AND Event Type=Translate AND Engine=DeepL), then the results reflect the logical AND of all filters. - Given a time range filter, when a custom start/end timestamp is set, then entries strictly within [start, end] inclusive are returned and others are excluded. - Given filters are cleared, when the user selects Reset Filters, then all filters are removed and the unfiltered ledger sorted by timestamp desc is shown. - Given a filter state, when the page is refreshed or the URL is shared, then the same filter state is restored from the URL query parameters.
Diff View with Glossary Highlights and Segment Integrity Indicators
- Given a translation event with source and translated segments, when the user opens Diff View, then source and translated text render side-by-side with insertions/deletions highlighted. - Given glossary matches exist, when Diff View is open, then glossary terms are highlighted and hovering/clicking reveals the matched term, glossary entry, and rule (exact/lemma) used. - Given an RTL target language, when Diff View is open, then the target column renders RTL correctly and alignment is preserved. - Given a segment’s hash verification is valid, when Diff View is open, then an integrity badge shows Verified; if the hash verification fails, then the badge shows Tampered with a tooltip indicating the failing segment ID.
Export to PDF, CSV, and JSON with Signatures, Hash Summaries, and Timestamps
- Given a user with export permission, when they export the current view as PDF/CSV/JSON, then the exported rows match the UI after filters and redaction are applied. - Given any export is generated, when the file is created, then it includes a cryptographic signature, a hash chain summary (root hash), and a UTC ISO 8601 generation timestamp in the document metadata or JSON top-level fields. - Given a redacted view, when an export is generated, then all sensitive fields remain redacted and the export metadata includes redacted:true; when unredacted view is exported by an authorized user, then redacted:false is present. - Given a PDF export with watermarking required, when it is generated, then each page contains a visible diagonal watermark and the export metadata records watermark:true. - Given a CSV export, when it is generated, then it conforms to RFC 4180 (quoted fields, comma delimiter, CRLF line endings) and includes a header row with field names.
API Endpoint Parity with UI for Programmatic Retrieval
- Given an API client with a valid token and Ledger:View scope, when it calls GET /api/v1/translation-ledger with objectId and filters, then the response status is 200 and the records match the UI for the same filters and object. - Given pagination parameters, when the client supplies limit and receives nextCursor, then subsequent calls with nextCursor return the next page until exhausted. - Given the client lacks Ledger:ViewSensitive, when requesting includeSensitive=true, then the API responds 403 and sensitive fields are redacted by default; with Ledger:ViewSensitive and includeSensitive=true, sensitive fields are returned. - Given any API response, when returned, then it includes signature, hashChainRoot, generatedAt (UTC ISO 8601), filtersApplied, and watermarked flags in the envelope. - Given rate-limited or unauthorized access, when the client exceeds limits or uses an invalid token, then the API returns 429 or 401 respectively with no ledger data in the body.
Hash Chain Integrity Detection and Reporting
- Given an intact ledger for an object, when integrity is checked, then the UI shows Integrity: OK with the root hash and the export includes the same status and hash. - Given a ledger entry is altered (simulated tamper), when integrity is checked, then the UI shows Integrity: Broken and identifies the first failing index; exports include integrityStatus:"Broken" and firstFailIndex. - Given a broken chain is detected, when a user attempts to export, then the export proceeds but includes a prominent integrity warning banner in PDF and integrityStatus:"Broken" in CSV/JSON metadata.
Dispute Evidence Bundle Generator
"As a treasurer, I want to produce a secure dispute evidence bundle in one click so that I can respond quickly to chargebacks with defensible documentation."
Description

Generate a one-click, read-only evidence bundle for a given bill or post that compiles the original content, all delivered translations, delivery records, user view/toggle history, engine/model attribution, and glossary/TM hits, with a signed summary page and verification hash. Produce a secure, expiring share link and track access events to the bundle. Include configurable templates to meet common processor and regulator requirements, and ensure exports meet size and format constraints for dispute portals.

Acceptance Criteria
One-Click Bundle Generation from a Bill
Given I am an authorized board member or manager viewing a bill or post detail page When I click Generate Evidence Bundle Then the system creates a read-only evidence bundle linked to that bill/post within 10 seconds for source content <= 50 pages and <= 100 translation entries And the bundle is assigned an immutable bundle ID and creation timestamp And the UI confirms success and provides a View Bundle action And the original bill/post remains unchanged
Bundle Contents and Attribution Integrity
Given a bundle is generated for a bill or post with delivered translations in the Translation Ledger When I open the bundle Then it contains exactly: original source content, all delivered translations with language codes, delivery records, user view/toggle history, engine/model attribution per translation, and glossary/TM hits per segment And counts of translations, deliveries, and views in the bundle match the ledger for that bill/post And every item is traceable to its ledger record by ID And no content from other bills/posts is present
Signed Summary Page and Verification Hash
Given a bundle has been generated When I inspect the first page Then it contains a summary with bill/post ID(s), bundle ID, creation timestamp, source checksum(s), and a cryptographic verification hash of the bundle payload And the bundle is signed with the platform signing certificate whose fingerprint is displayed And when I verify the signature and hash using the public verification endpoint or tool, validation succeeds And if any file in the bundle is altered, hash validation fails
Secure Expiring Share Link Generation
Given a generated bundle When I create a share link with an expiry of 7 days Then the system issues an HTTPS URL containing an unguessable token with at least 128 bits of entropy And the link grants read-only access to the bundle without edit capabilities And before expiry the link resolves and the bundle is viewable/downloadable And after expiry the link returns 410 Gone and access is denied And I can revoke the link prior to expiry, after which access is denied
Access Event Tracking for Evidence Bundle
Given a share link exists When the link is accessed, attempted, or revoked Then an access event is recorded with timestamp (UTC), bundle ID, action (view/download/attempt/revoke/expire), requester IP and user-agent, and outcome (success/denied/expired) And events are immutable and queryable by bundle ID within the admin audit UI and API And exporting events to CSV/JSON returns exactly the recorded data
Template Selection and Compliance Output
Given admin-configured templates exist (e.g., Processor Template A, Regulator Template B, Internal Default) When I generate a bundle and select a template Then the bundle layout, required sections, and metadata conform to the selected template’s schema And validation against the template schema passes with zero errors And the selected template name and version are recorded on the summary page
Export Size and Format Constraints Enforcement
Given a template defines export constraints: format=PDF/A-2b, max_total_size_mb=10, max_pages=500, max_embedded_images_per_page=10 When I generate the bundle with that template Then the system optimizes content (compression, image downsampling) to meet the constraints without altering textual content And if the bundle fits within constraints, generation succeeds and output validates as PDF/A-2b And if constraints cannot be met, generation fails with a clear error listing which limits were exceeded and by how much
Notification Localization Delivery Logging
"As a part-time manager, I want to see the exact translated reminder each homeowner received and its delivery status so that I can address claims of non-receipt or misunderstanding."
Description

Record localization details for every outbound announcement or reminder, including selected locale, rendered content hash, personalization variables, channel (email/SMS/in-app), provider message IDs, delivery timestamps, and delivery outcomes (sent, bounced, failed) with reasons. Link each delivery record to the corresponding translation ledger entries and the target user. Provide reconciliation reports that show exactly which translated variant each recipient received and when.

Acceptance Criteria
Per-Delivery Localization Metadata Logged
- Given an outbound announcement or reminder is sent to a user via any channel When the message is rendered for delivery Then a delivery record exists that includes: post_or_bill_id, user_id, channel, selected_locale, rendered_content_hash, personalization_variables (keys and resolved values), queued_at timestamp. - Given the provider returns a message ID When the provider acknowledges the send Then provider_message_id is stored on the same record. - Given the final payload sent to the provider When hashing is performed Then rendered_content_hash reflects the localized and personalized content actually sent.
Delivery Outcome and Reason Capture
- Given a message has been attempted When the provider response indicates accepted Then outcome = "sent" and outcome_reason is null. - Given a provider bounce occurs When the provider returns a bounce or undeliverable code Then outcome = "bounced" and outcome_reason stores both the provider code and a normalized_reason mapped to a standard taxonomy. - Given a failure occurs before reaching the provider (e.g., template render error) When the send is aborted Then outcome = "failed" and outcome_reason records the failure type and details. - Given multiple attempts are made to the same user/post/channel When retries occur Then each attempt is recorded with attempt_number and timestamps, and the latest attempt determines the final outcome field.
Linkage to Translation Ledger and Target Entities
- Given localized content is produced When a delivery record is created Then it links to the translation_ledger_entry_id(s) used, the source post_or_bill_id, and the target user_id. - Given no translation was required (source equals selected locale) When a delivery record is created Then a base_content_version_ref is stored and a no_translation flag is set. - Given any linked ID When referential integrity is validated Then all links resolve (no broken or missing references).
Reconciliation Report by Post/Bill
- Given a post or bill with deliveries across channels and locales When an admin generates a reconciliation report for that post/bill Then the report lists one row per delivery including: user_id, channel, selected_locale, translation_ledger_ref(s), rendered_content_hash, provider_message_id (if any), queued_at, sent_at (if any), outcome, outcome_reason. - Given the report is generated When totals are computed Then the number of rows equals the count of delivery records for that post/bill. - Given the report is exported When CSV export is requested Then the CSV contains the same rows and columns as the on-screen report.
Selected Locale Determination and Fallback Logging
- Given a recipient's preferred locale is unsupported for the content When the system selects a fallback locale per policy Then selected_locale reflects the fallback and fallback_reason is recorded on the delivery record. - Given glossary terms are applied during translation When content is localized Then the translation ledger linked to the delivery includes the glossary hit details for audit.
Rendered Content Hash Determinism and Uniqueness
- Given two deliveries with identical template version, locale, and personalization values When hashes are computed Then rendered_content_hash values are identical. - Given any input differs (template version, locale, or personalization value) When hashes are computed Then rendered_content_hash values differ. - Given a delivery record appears in a reconciliation report When the hash is recomputed from the archived payload Then the recomputed hash equals the stored rendered_content_hash.

Auto Terms Builder

One click proposes a compliant payment plan tailored to the member’s balance, age of debt, and community policy. Admin-set rules (min payment, max duration, fees/interest, start date windows) auto-populate a clear schedule and message. Delivers consistent, defensible terms in seconds—reducing back‑and‑forth and speeding enrollment.

Requirements

Policy Rule Engine & Admin Config
"As a board admin, I want to define clear payment plan rules so that the system can propose consistent, compliant terms without manual calculations."
Description

Provide a configurable rules engine that allows community admins to define plan parameters such as minimum payment (amount or % of balance), maximum duration (months), interest/fee model (APR or flat; upfront vs per-installment), grace periods, start date windows, rounding rules, minimum installment thresholds, and small-balance forgiveness. Support multiple rule sets by balance tiers and debt age with effective dates, versioning, and per-community overrides. Include a configuration UI with validation, inline help, and a sandbox preview showing example outcomes. Enforce role-based permissions, change history, and rollback to prior versions. Persist policies to power consistent proposal generation across web and mobile contexts.

Acceptance Criteria
Minimum payment, rounding, thresholds, and small-balance forgiveness
Given a policy with minPayment = higher of 10% of balance or $50, minInstallmentThreshold = $25, roundingRule = nearest $5, and smallBalanceForgiveness = balances < $30 forgiven When an admin previews a plan for balances of $430 and $20 in the sandbox and generates a plan for a $430 member Then for $430: each installment amount is >= $50 after rounding and is a multiple of $5 And for $430: no installment is below $25 And for $430: total scheduled principal equals $430 And for $20: no plan is created; the system flags the balance as forgiven with reason "below threshold" and logs the event
Max duration with start date windows and grace period
Given a policy with maxDuration = 12 months, startDateWindow = days 1–10 of each month, gracePeriod = 7 days, and minInstallmentThreshold = $50 When a plan is generated on 2025-08-16 for a $600 balance Then the first installment date is scheduled for 2025-09-01 (moved to the next allowed window after applying grace) And the plan contains no more than 12 installments And if applying 12 installments would yield any installment < $50, the engine shortens the duration until all installments are >= $50 and <= 12 months
Interest/fee model: APR vs flat; upfront vs per-installment
Given a policy with interestModel = APR 12% compounding monthly on declining balance, perInstallmentFee = $2, upfrontSetupFee = $10 When a 6-installment plan is generated for $600 starting 2025-09-01 Then each installment shows principal, interest, and fees as separate line items And the sum of interest across the schedule equals the amortized interest for 12% APR over 6 months within ±$0.01 And the $10 setup fee is applied only to the first installment When the policy is changed to flatInterest = $60 with upfront application Then the first installment includes $60 interest and subsequent installments include $0 interest When the policy is changed to flatInterest = $60 with per-installment application Then each of the 6 installments includes $10 interest
Rule-set selection by balance tier, debt age, effective date, and community override
Given balance tiers: A (< $500) and B ($500–$2,000); default RuleSet B v2 effective 2025-08-01; community override RuleSet C for debtAge > 90 days effective 2025-08-10 When a member with balance $700 and debtAge 120 days requests a plan on 2025-08-16 in Community X Then the applied policy is Community X RuleSet C latest active version as of 2025-08-16 And the response contains appliedPolicyId and appliedPolicyVersion When the request date is 2025-07-31 Then the applied policy is default RuleSet B v1 (v2 not yet effective) When no community override exists and debtAge <= 90 days Then the applicable default tiered rule set is selected based on balance tier alone
Config UI: validation, inline help, and sandbox preview
Given an admin is on the Policy Configuration UI When entering invalid values (e.g., APR < 0, startDateWindow end < start, minInstallmentThreshold > minPayment absolute) Then inline validation messages are shown per field, invalid fields are highlighted, and Save/Publish is disabled When hovering or tapping help icons next to fields Then inline help text describes the rule, allowed ranges, and examples When changing sample balance and debt age in the sandbox preview without saving Then the preview recalculates using the unsaved inputs and displays an example schedule and total charges consistent with the current form values
Permissions, change history, and rollback
Given roles: Admin (edit), Manager (view), Viewer (view) When a Manager or Viewer attempts to create, edit, publish, or rollback a policy Then the action is blocked with a 403/Unauthorized message and no changes are persisted When an Admin publishes a policy change Then a history entry records user, timestamp, effective date, before/after values, and a change note When an Admin rolls back to a prior version Then the selected version's values become the active policy as a new version, and the history records the rollback link between versions
Persistence and cross-platform proposal consistency
Given policies are persisted per community with versioning and exposed via API When proposals are generated for the same member, balance, and timestamp from web and mobile Then both proposals use the same appliedPolicyVersion and produce identical schedules and totals When a policy is updated and published Then subsequent proposals reference the new appliedPolicyVersion, and any cached policy data is invalidated before calculation
Eligibility Assessment & Plan Recommendation
"As a community manager, I want the system to automatically assess eligibility and propose a best-fit plan so that I can offer terms quickly and fairly."
Description

Implement server-side logic to evaluate each member’s delinquency context (balance, age of debt, prior plan history, account status/holds, policy effective dates) and determine eligibility. When multiple policies apply, score and select the best-fit plan using admin-defined priorities (e.g., shortest compliant term, lowest monthly installment). Present a clear rationale and ineligibility reasons with codes. Recalculate recommendations in real time if balance or policy inputs change. Expose endpoints and events for UI consumption and logging, and include robust unit tests for edge cases.

Acceptance Criteria
Eligibility Determination with Rationale Codes
Given a member record with balance, ageOfDebtDays, priorPlanHistory, accountStatus, holds, and applicable policy configurations with effective dates When the eligibility service evaluates the member Then the result includes eligible (true|false) and is deterministic for identical inputs And the result includes rationale items each with code, message, and source (e.g., POLICY_RULE, ACCOUNT_STATUS) And if eligible is false, at least one ineligibility code is present and recommendation is null And if eligible is true, an ELIGIBLE code is present and recommendation contains at minimum policyId and score
Multi-Policy Scoring and Priority Selection
Given two or more policies are applicable by effective date and the member meets each policy's base rules And admin-defined priorities are configured as an ordered list (e.g., SHORTEST_TERM, LOWEST_MONTHLY, LOWEST_TOTAL_COST, EARLIEST_START_DATE) When the evaluation runs Then the service computes comparable metrics per policy for each priority dimension And selects a single best-fit policy according to the priority order And applies tie-breakers in order: next priority left-to-right, then LOWEST_TOTAL_COST, then lowest policyId lexicographically And returns the selected policyId with a ranked list of compared policies including per-dimension reasons and scores And emits a recommendation.selected event with selectedPolicyId and comparedPoliciesCount
Real-time Recalculation on Balance or Policy Change
Given a member's balance changes or an admin updates policy inputs (e.g., minPayment, maxDuration, startDateWindow) When the change is persisted Then eligibility and recommendation are recomputed and available within 500ms P95 and 1000ms P99 And an eligibility.recalculated event is emitted including changeReasons and previousRecommendationId (if any) And the latest API response reflects the new recommendation and version increments by 1
Account Holds and Status Enforcement
Given accountStatus is in {SUSPENDED, LEGAL, BANKRUPTCY} or an active administrative Hold exists When eligibility is evaluated Then eligible is false and recommendation is null And rationale includes HOLD_ACTIVE and/or ACCOUNT_STATUS_BLOCKED with referenced holdIds or status values And an audit log entry records evaluatorId, memberId, timestamp, and blocking factors
Policy Effective Dates and Timezone Compliance
Given a communityTimeZone is configured and policies specify effectiveStart and effectiveEnd timestamps When evaluation occurs at a specific instant Then only policies effective at that instant in communityTimeZone are considered And computed startDate windows and term boundaries use communityTimeZone and never select a date before the next allowed start date And date calculations are consistent across DST changes and month-ends (no off-by-one-day errors)
API Contract for Eligibility and Recommendation
Given an authenticated client calls the evaluation API When the request is valid Then the response is 200 and matches the schema: { eligible, ineligibilityReasons:[{code,message,source}], recommendation:{policyId,termMonths,monthlyInstallment,startDate,totalCost,score,rationale:[{dimension,value,rank}]}, comparedPolicies:[...] , version } And on validation errors the API returns 400 with machine-readable error codes; on unknown member 404; on concurrent modification 409 And the OpenAPI contract is published and contract tests pass against the running service
Unit Tests and Edge Case Coverage
Given the eligibility and scoring modules When the unit test suite runs in CI Then statement and branch coverage are each >= 85% for these modules And test cases include: zero balance, credit (negative) balance, cooldown period from prior plan, conflicting policy rules, exact tie across all priorities, timezone boundary at midnight, and missing config defaults And all listed edge case tests pass
Schedule Generator & Interest/Fee Calculations
"As an admin, I want an accurate installment schedule with interest and fees applied per policy so that the terms are clear and enforceable."
Description

Generate an itemized installment schedule that adheres to the selected policy, including number of installments, due dates, principal/interest/fee breakdowns, rounding to currency precision, and last-payment adjustments. Support APR-based interest (monthly or daily accrual) and flat fees (upfront or per installment). Enforce minimum installment and max duration constraints, adjust for weekends/holidays per community settings, and honor start date windows. Allow pre-acceptance recalculation if the balance changes; lock the schedule upon acceptance. Output bill-ready line items compatible with Duesly’s ledger and billing services.

Acceptance Criteria
Monthly APR schedule with currency rounding and last-payment adjustment
Given a policy with monthly APR interest accrual and a configured currency precision And a member balance B and N installments within policy limits And a start date within the allowed window When the installment schedule is generated Then each installment includes principal, interest, and fee fields And each line item amount is rounded to the configured currency precision And the sum of principal across all installments equals B exactly And the sum of interest equals the computed monthly APR interest with any rounding remainder applied to the final installment And no installment has a negative amount in any component And the final installment adjustment does not exceed one unit of the smallest currency increment
Minimum installment and maximum duration enforcement
Given policy constraints of minimum installment M and maximum duration D (in periods) And a balance B and preferred start date S When the schedule is generated Then the algorithm selects an installment count N such that N ≤ D And each installment total (principal + interest + per‑installment fees) is ≥ M except the final installment which may differ only due to rounding and remaining balance And if no such N satisfies the constraints, the generator returns a validation error with code CONSTRAINTS_UNSATISFIABLE and enumerates the blocking constraints (e.g., minInstallment, maxDuration) And for a valid schedule, the first due date is on or after S and total duration does not exceed D periods
Weekend/holiday date adjustments and start date window honoring
Given community settings define a start date window [Smin, Smax] relative to today And a weekend/holiday adjustment rule R (next business day | previous business day | no change) And a holiday calendar H When the schedule is generated Then the first due date falls within [Smin, Smax] after applying rule R And no due date falls on a weekend or a date in H And any due date that would fall on a weekend/holiday is shifted according to R And the selected cadence (e.g., monthly on day X) is preserved across the schedule after shifts
Flat fee handling (upfront and per‑installment)
Given policy defines an upfront flat fee F0 (optional) and a per‑installment fee F1 (optional) When the schedule is generated Then if F0 is present, an upfront fee line item is created with due date aligned per policy (default: first due date) And each installment line item includes a fee component equal to F1 if configured And schedule totals equal B + totalInterest + F0(if any) + (F1 × N if any) And fee amounts are rounded to currency precision, with any rounding remainder applied to the final applicable installment And removing F0 or F1 from policy omits them from the output without affecting other calculations
Daily APR interest accrual correctness
Given a policy with APR r using daily accrual And period boundaries defined by consecutive due dates When the schedule is generated Then interest for each period equals round_to_precision(period_principal × r/365 × days_in_period) And the sum of per‑period interest equals the interest total shown in the schedule after distributing any rounding remainder to the final installment And modifying start date or due date spacing updates days_in_period and recomputes interest accordingly
Pre‑acceptance recalculation and post‑acceptance lock
Given a proposed schedule in status Proposed And a balance change Δ occurs before member acceptance When the balance changes Then a new schedule version is generated using current policy and the prior proposal is marked Superseded with an audit record (who, when, why) And upon member acceptance, the schedule status becomes Locked and regeneration/editing is disabled And bill‑ready line items are emitted to Duesly billing with an immutable scheduleId and idempotency keys And subsequent balance changes do not alter the locked schedule and are surfaced as separate charges per policy And the billing payload passes integration contract validation for required fields (dueDate ISO‑8601, currency, amount, breakdown)
One-Click Proposal & Feed Message Composer
"As a manager, I want to generate and send a ready-to-accept plan with one click so that I reduce back-and-forth and enroll members faster."
Description

Enable one-click generation of a payment plan proposal from a member ledger or delinquency post, auto-populating a human-readable summary, full schedule, fees/interest explanations, acceptance CTA, and required disclaimers. Provide a preview with editable message sections and admin-approved templates. Publish the proposal to the Duesly feed and deliver via email/SMS/push based on member preferences, with localization and accessibility support. Save drafts, track opens and link clicks, and store a canonical proposal snapshot tied to the policy version.

Acceptance Criteria
One-Click Proposal Generation from Ledger or Delinquency Post
Given an admin viewing a member ledger with an overdue balance and applicable policy rules exist When the admin clicks "Propose Terms" Then a payment plan is generated that meets min payment, max duration, fee/interest, and start date window constraints And the plan includes a human-readable summary, full installment schedule, fee/interest explanations, acceptance CTA, and required disclaimers. Given a delinquency feed post for the same member When the admin clicks "Propose Terms" on the post Then an equivalent plan is generated and pre-linked to the post context. Given the balance or rules make a compliant plan impossible When the admin clicks "Propose Terms" Then generation is blocked and a message lists the specific failing constraints with guidance to adjust policy or balance. Given the member already has an active plan When the admin attempts to generate new terms Then the system requires explicit confirmation to supersede and records the decision in the audit log.
Proposal Preview and Editable Composer with Templates
Given a generated proposal exists When the preview opens Then the message is composed from the selected admin-approved template with placeholders resolved And only designated sections are editable while locked sections remain read-only. Given the admin edits content When saving the draft Then required disclaimers must be present and template constraints enforced; otherwise save is blocked with inline errors identifying missing/violated elements. Given placeholders for amount, dates, and schedule When plan parameters change Then dependent placeholders update in the preview before publish. Given a template version is selected When saving Then the template ID and version are stored on the draft.
Publish to Feed and Multi-Channel Delivery per Member Preferences
Given a valid proposal draft When the admin clicks Publish Then a feed post is created with correct member visibility and a unique proposal link. Given the member’s delivery preferences include email, SMS, and/or push When the proposal is published Then notifications are sent over each opted-in channel with localized, channel-appropriate formatting. Given a channel is undeliverable (bounce, invalid number, denied push) When publishing Then the failure is logged and surfaced to the admin And at least one other available channel is attempted if configured. Given Do Not Contact for marketing is enabled but compliance communications are permitted When publishing Then only compliance-permitted channels are used.
Draft Autosave and Resume
Given an admin is composing a proposal When changes are made Then the draft autosaves within 5 seconds of inactivity and on navigation or close. Given a saved draft exists When the admin reopens the composer Then the draft loads with all edits, selected template, and plan parameters intact. Given two admins open the same proposal draft When one saves Then the other receives a newer-version warning and must resolve before publishing to prevent overwrite. Given a draft is inactive for 30 days When accessed Then the admin is prompted to confirm continuation due to potentially outdated policy.
Open and Click Tracking with Analytics
Given notifications are sent When the member opens an email or taps a push/SMS link Then an open or click event is recorded with timestamp, channel, and proposal ID. Given a member uses multiple devices or repeats actions When events are processed Then events are deduplicated per device-event type within a 24-hour window. Given tracking is disabled for a member by policy When notifications are sent Then tracking pixels and click redirects are omitted for that member. Given events are recorded When the admin views proposal analytics Then total opens, unique opens, clicks, and last activity time are displayed.
Canonical Proposal Snapshot and Policy Versioning
Given a proposal is published When publishing completes Then an immutable snapshot is stored containing message content, schedule, amounts, fee/interest calculations, policy ID and version, and localization settings. Given live policy rules change after publishing When viewing the proposal Then the snapshot displays the original terms and references the original policy version. Given a snapshot exists When exported or used for disputes Then it includes a cryptographic hash and timestamp for integrity verification.
Localization, Formatting, and Accessibility Compliance
Given the member’s preferred language and locale are known When composing and sending Then dates, currency, numbers, and time zones are formatted per locale And the message uses the corresponding template translation or a defined fallback flagged to the admin. Given the preview and published feed post When evaluated with accessibility tools Then they meet WCAG 2.2 AA for contrast, focus order, ARIA roles, keyboard navigation, and alt text for non-text content. Given the acceptance CTA in each channel When used with screen readers and high-contrast modes Then the CTA is labeled, focusable, and operable via keyboard without pointer input. Given long schedules or disclaimers When sending via SMS or push Then content is truncated responsibly with a link to the full proposal without omitting required legal text.
Compliance Guardrails, Caps & Audit Trail
"As a board treasurer, I want automated guardrails and a complete audit trail so that every plan is consistent, defensible, and compliant."
Description

Apply jurisdictional and community caps (e.g., maximum interest, fee limits, max term) and block proposals that would exceed them. Provide safe overrides within configured bounds that require justification and, if enabled, second-level approval. Maintain an immutable audit trail capturing policy version, input data, calculated outputs, edits, approvers, timestamps, and delivery channels. Offer export and read-only views for board reviews and disputes. Surface inline compliance warnings and reasoning to ensure consistent, defensible terms.

Acceptance Criteria
Block Proposals That Exceed Caps
Given jurisdictional and community caps are configured (max APR interest, per-fee limit, total fees cap, max term length) and a member balance and debt age are provided When Auto Terms Builder generates a payment plan Then the system must block any plan where calculated interest rate > configured max APR, any single fee > per-fee limit, aggregate fees > total fees cap, or term length (months) > max term And the UI displays specific blocking errors identifying each breached rule and its configured limit And the system prevents saving or sending the proposal until terms are adjusted to comply And a cap_violation_attempt event is appended to the audit trail with details of the violations
Bounded Override With Justification and Approval
Given override bounds are configured for each rule and the user has Override permission When an admin attempts an override within bounds and enters a justification of at least 20 characters Then the system applies the override, marks the plan as Overridden, and records overridden fields and justification in the audit trail And when Require Second-Level Approval is enabled for that rule Then the plan status becomes Pending Approval, an approver with Approval permission (not the requester) is notified, and the plan cannot be sent until approved And when the approver approves, the plan status becomes Approved and override takes effect; when rejected, the override is revoked and the plan reverts to compliant defaults And all approval/rejection actions capture approver userId, timestamp, and comments in the audit trail
Immutable Audit Trail Captures Full Context
Given any create, calculate, edit, override, approval, send, or export action occurs on a plan When the action completes Then an audit entry is appended capturing: policyVersionId, policy effective dates, input data snapshot (memberId, balance, debt age, config values), calculated outputs (schedule, rates, fees), userIds, action type, previous and new values, timestamps (UTC ISO 8601), delivery channels and message IDs (if sent) And audit entries are immutable and append-only: no API or UI can modify or delete existing entries; attempted updates return 403 And each audit entry includes a content hash and chained previous-hash to detect tampering And exports and read-only views reproduce exactly the data from the audit entries
Export and Read-Only Review Views
Given a user with Board Review permission selects a plan When they choose Export Then the system generates PDF and CSV exports including plan terms, schedule, all fees/interest calculations, policyVersionId, compliance warnings, overrides, approvals, timestamps, and full audit trail within 10 seconds And the filename includes communityId, memberId, planId, and ISO timestamp And when a read-only share link is generated Then the link is view-only, expires by default in 14 days (configurable), and all accesses are logged in the audit trail And read-only views do not expose edit, override, or approve controls
Inline Compliance Warnings and Reasoning
Given configured caps and rules When plan calculations are within 95% of any cap Then the UI displays a non-blocking Warning with explanation referencing the rule name and threshold value And when any cap is exceeded Then the UI displays a blocking Error with explicit rule, configured limit, calculated value, and suggested adjustments And expanding the message reveals the reasoning: the formula and inputs used to determine the breach And all warnings and errors are captured in the audit trail with ruleId and values
Policy Versioning Applied at Proposal Time
Given policy versions have effective date ranges When a plan is generated Then the policyVersionId effective at generation is bound to the plan and recorded in the audit trail and UI And when policies are updated later Then existing plans retain their original policyVersionId and are unaffected; re-calculation creates a new proposal tied to the current policyVersionId while preserving the prior in the audit trail And exports and read-only views display the bound policyVersionId and effective dates
Enrollment & E‑Sign Consent Flow
"As a member, I want to easily review and accept a payment plan from my phone so that I can resolve my balance without confusion."
Description

Provide a mobile-first member experience to review the summary and full terms, select payment method, opt into autopay, and accept via compliant e-sign (ESIGN/UETA) with captured IP, device, timestamp, and consent text. Generate and store an executed agreement PDF and attach it to the member record. Support decline with reason, request-changes flow, and admin countersign (optional). Handle accessibility, localization, and session timeouts gracefully. On acceptance, lock terms and propagate events to billing and reminders.

Acceptance Criteria
Mobile Review of Summary and Full Terms
Given a member opens the enrollment link on a mobile device (≤414px width), when the flow loads, then the summary shows total balance, installment count, first due date, per‑installment amount, and any fees within 1.5s. Given the summary is visible, when the member taps "View Full Terms", then the full terms display in a scrollable view and the back action returns to the summary without losing inputs. Given the full terms exist, when the member has not opened or scrolled them to 80%, then the "Accept" action remains disabled. Given intermittent network, when content is loading, then a skeleton/loading indicator is shown and retry is offered on failure without losing inputs. Given the member navigates between steps, when returning to this step, then scroll position and any selections are preserved.
Payment Method Selection and Autopay Opt‑In
Given a generated plan schedule, when the member selects a payment method (ACH, card, or saved method), then the next‑charge date and amount (including fees) update immediately. Given ACH is selected, when entering routing and account numbers, then format validation occurs client‑side and an ACH authorization text is displayed with a required consent checkbox. Given card is selected, when using a new card, then card tokenization succeeds before proceeding and only last4/brand are stored; for a saved card or bank, the last4/brand are prefilled. Given Autopay toggle is off by default, when the member enables Autopay, then a required "recurring debit authorization" checkbox appears and must be checked to continue. Given an invalid or incomplete payment method, when the member attempts to continue, then an inline error is shown and progression is blocked until valid.
ESIGN/UETA Consent and Signature Capture
Given ESIGN/UETA compliance is required, when the consent screen displays, then the ESIGN disclosure text (versioned) is shown with a required "I consent" checkbox and a link to download/print. Given the member has not checked consent, when attempting to proceed, then progression is blocked with an accessible error message. Given the member consents and signs (typed name or drawn signature), when submitting, then IP address, device user‑agent, UTC ISO‑8601 timestamp, consent text version, and signature payload are captured and stored atomically. Given the member declines ESIGN consent, when selecting decline, then the e‑sign flow is exited and the decline path is initiated without storing a signature.
Executed Agreement PDF Generation and Attachment
Given acceptance is submitted, when processing completes, then a PDF is generated within 10 seconds containing the summary, full terms, payment schedule, consent text, signature image/typed name, IP, user‑agent, UTC timestamp, and document hash (SHA‑256). Given the PDF is generated, when saving to storage, then it is attached to the member record with a unique document ID and immutable version number. Given the PDF is stored, when accessed by the member or an authorized admin, then it downloads or renders within 2 seconds and matches the accepted terms (same version and totals). Given access control, when an unauthorized user attempts to access the PDF, then access is denied and logged.
Decline with Reason and Request‑Changes Flow
Given the member chooses to decline, when submitting a decline, then a reason (minimum 10 characters) is required, the plan remains unenrolled, and an audit event is recorded and sent to admins. Given the member chooses "Request Changes", when submitting the request, then allowed fields per policy (e.g., start date within window, duration within min/max) are validated and included; otherwise a free‑text message is required. Given a change request is submitted, when processing completes, then the agreement status becomes "Change Requested", no billing/autopay events are emitted, and admins are notified with the request payload. Given the member exits after decline/request, when returning via the enrollment link, then the prior decline/request state is shown with options to start a new negotiation if enabled by policy.
Finalization: Lock Terms, Optional Admin Countersign, and Event Propagation
Given community policy requires admin countersign, when the member accepts, then the agreement state becomes "Pending Admin Countersign" and the member is shown a confirmation of pending status. Given an admin countersigns, when submission completes, then the state becomes "Active", the PDF is updated to include countersign metadata (name, title, UTC timestamp), and an audit event is recorded. Given community policy does not require countersign, when the member accepts, then the state becomes "Active" immediately. Given the state becomes Active, when finalization runs, then terms and schedule are locked (read‑only), a lock timestamp and version are recorded, and edit APIs return 409 Conflict. Given finalization succeeds, when events are published, then AgreementActivated, PaymentScheduleCreated, AutopayEnabled (if applicable), and RemindersScheduled events are emitted with an idempotency key; duplicates are not created on retry. Given event publishing encounters transient failure, when retry logic executes, then up to 3 retries with exponential backoff occur and the agreement remains Active with a warning surfaced to ops telemetry.
Accessibility, Localization, and Session Timeout
Given a user relies on assistive tech, when navigating the flow, then all interactive elements are reachable by keyboard, have visible focus, and expose proper names/roles/states to screen readers (WCAG 2.1 AA). Given the interface renders on small screens, when viewed at 320px width and 200% zoom, then content reflows without loss of information or functionality and maintains contrast ratios >= 4.5:1. Given the community locale is configured, when the flow displays text, dates, and currency, then they are localized to that locale and a fallback to English is provided if a translation is missing. Given inactivity, when 28 minutes have elapsed without interaction, then a modal warns of timeout; at 30 minutes, the session expires, inputs to the current step are autosaved as a draft, and a secure resume link (valid 24 hours) is emailed if an email is on file. Given the session is resumed via the link, when the member authenticates, then the flow restores the last completed step and saved inputs; any expired terms are revalidated before allowing acceptance.
Reminder & Billing Automation Integration
"As a manager, I want reminders and billing to run automatically once a plan is accepted so that payments stay on track without manual follow-up."
Description

Upon acceptance, automatically instantiate scheduled invoices in Duesly, enroll the member in reminder cadences (pre-due, due-day, past-due), and log all notifications. Pause or supersede existing collection reminders to avoid duplicates. Handle missed installment logic (grace period, reinstatement options, escalation), early payoff, and plan cancellation upon payoff. Keep the ledger synchronized with partial payments and adjustments, and notify stakeholders of significant events. Provide dashboards and filters to monitor plan health and success rates.

Acceptance Criteria
Auto-create Scheduled Invoices on Plan Acceptance
Given an approved payment plan with N installments, a defined schedule, community timezone, and a unique plan_id And the member completes plan acceptance via Duesly (e-sign or explicit consent) When the acceptance webhook/event is received Then the system creates N scheduled invoices with amounts and due dates matching the plan schedule in the community timezone within 5 seconds And each invoice is linked to the plan_id, member_id, and original balance_id And invoice terms (amount, due_date, late_fee policy) mirror the plan configuration And the operation is idempotent such that reprocessing the same acceptance event does not create duplicate invoices And the plan status becomes "Active" and is visible on the member and admin views
Enroll Member in Reminder Cadences and Log Notifications
Given community reminder cadences are configured for pre-due, due-day, and past-due with offsets and channels And a plan is activated with scheduled invoices When reminders are scheduled Then for each invoice the system creates reminder jobs per cadence offsets at 09:00 in the community timezone unless overridden And each reminder includes amount due, due date, plan_id, pay link, and installment number And a Notification Log entry is stored for each scheduled and sent reminder with timestamp, channel, template_id, recipient, status, and correlation_id And duplicate reminders for the same invoice and channel within a 24-hour window are prevented
Pause/Supersede Existing Collection Reminders to Prevent Duplicates
Given the member has active general collection reminders tied to the same balance_id And a payment plan is activated for that balance When the plan activation is processed Then all general collection reminders for that balance_id are paused within 10 seconds and marked superseded_by=plan_id And any reminder scheduled to send within the next 15 minutes that targets the same amount/invoice is canceled And audit records show which reminders were paused/canceled with timestamps and user/system actor And no member receives both a general reminder and a plan reminder for the same invoice on the same day
Missed Installment Handling: Grace, Reinstatement, and Escalation
Given a plan installment reaches its due date unpaid When the community grace_period_days > 0 Then the installment status becomes "In Grace" and a grace notice is sent once at due_date+0h And no past-due reminder is sent during the grace period When the grace period expires and the installment remains unpaid Then the installment status becomes "Past Due", a past-due reminder cadence starts, and any configured late fee/interest is applied exactly once And if payment is received within reinstatement_window_days, the plan returns to "Active" and future escalations for that installment are cleared And if missed_installments_threshold is reached (e.g., 2 consecutive), the plan status becomes "Escalated" and the escalation workflow is triggered
Early Payoff: Settle Plan, Stop Invoices and Reminders
Given a member submits a payment equal to or greater than the remaining plan balance When the payment settles successfully Then all remaining scheduled invoices are canceled and marked paid_via_early_payoff=true And all pending reminder jobs for the plan are canceled And the plan status is set to "Completed" with completion_reason="Early Payoff" And a confirmation is sent to the member and admins with a zero-balance statement And no additional late fees or interest are applied after the payoff timestamp
Ledger Sync with Partial Payments and Adjustments
Given a partial payment is received against a plan installment When the payment settles Then the ledger reflects the applied amount, the installment's remaining_due is reduced accordingly, and receipts link to plan_id and invoice_id And reminder content and amounts are updated to reflect the new remaining_due before the next reminder send When an admin posts an adjustment (credit/debit) to the plan or installment Then the remaining schedule is recalculated per policy within 60 seconds, preserving due dates unless policy requires reflow And all ledger changes are immutable, with reversal entries rather than destructive edits
Stakeholder Notifications and Plan Health Monitoring
Given event types are defined: Plan Activated, Installment Paid, Missed Payment, Reinstated, Escalated, Early Payoff, Completed, Canceled When any such event occurs Then notifications are sent to configured stakeholders (roles: Board, Manager, Treasurer) via in-app and email within 2 minutes, respecting user notification preferences And each notification is logged with event_type, recipients, delivery status, and correlation_id And the Plan Health dashboard displays counts and rates by status (Active, In Grace, Past Due, Escalated, Completed, Canceled), completion rate, on-time rate, average days delinquent, and total outstanding balance And dashboard filters include community, manager, status, start_date range, risk flags (missed >=1), and saved views And dashboard metrics recalculate within 5 minutes of ledger changes and match ledger totals within 1%

Plan Autopay

Enable automatic installment payments via ACH or card so members never miss a due date. Smart retries and instant receipts keep homeowners informed while lifting on‑time rates. Treasurers gain predictable cash flow without chasing checks.

Requirements

Autopay Enrollment & Consent
"As a homeowner, I want to easily enroll in autopay for my dues with clear consent so that my payments run automatically and I never miss a due date."
Description

Provide a guided flow for homeowners to enroll in autopay for specific dues and assessment plans, capturing explicit authorization text and acceptance for ACH and card payments. Allow opt-in at the unit/account level with per-plan overrides, payment caps, default funding source selection, and policy checks (e.g., block enrollment if account is in collections or has disputed charges). Persist consent artifacts (timestamp, IP/device, user, mandate text version) and tie them to the member, unit, and plan. Automatically bind enrollment to bills generated from posts so scheduled payments are created without manual steps. Include self-service controls to pause, skip one installment, resume, or cancel with immediate effect and comprehensive auditability.

Acceptance Criteria
ACH Autopay Enrollment & Consent Capture
Given a homeowner with an eligible unit and a verified ACH funding source When they initiate autopay enrollment for a specific dues/assessment plan and reach the consent step Then the current ACH mandate text version is displayed and explicit acceptance is required (checkbox + confirm) And upon confirmation, the system creates an active enrollment tied to the member, unit, and plan And consent artifacts are persisted: timestamp (UTC ISO‑8601), user ID, unit ID, plan ID, funding source ID, IP address, device identifier, mandate text version, consent method And an immutable audit log entry is created and visible to admins and the homeowner And the enrollment status is reflected in UI within 5 seconds
Card Autopay Enrollment & Consent Capture
Given a homeowner with an eligible unit and a saved/verified card funding source When they initiate autopay enrollment for a specific dues/assessment plan using card and accept the card authorization text Then the system records consent artifacts: timestamp (UTC ISO‑8601), user ID, unit ID, plan ID, funding source ID, IP address, device identifier, authorization text version, consent method And creates an active card autopay enrollment tied to the member, unit, and plan And an immutable audit record is written and is retrievable by admins and the homeowner And the UI confirms enrollment success within 5 seconds
Enrollment Policy Checks & Blocking Rules
Given a homeowner attempts to enroll in autopay for a plan When the account is in collections OR has any open disputed charges OR the funding source is unverified OR the plan is ineligible (closed/archived) Then enrollment is blocked and no consent is recorded And the user sees a specific reason message and code for the block And an audit entry is recorded with the attempted enrollment details and block reason And the system allows retry only after the blocking condition is cleared
Unit Default Autopay with Per‑Plan Overrides & Payment Caps
Given a unit has a default autopay setting (funding source A, cap $X) enabled And a homeowner configures an override for Plan P (funding source B, cap $Y) When bills are generated for Plan P and for Plan Q (no override) Then Plan P uses funding source B and cap $Y, while Plan Q inherits funding source A and cap $X And if a plan installment total exceeds its cap, the installment is not charged, the user is notified, and the event is logged And changing the unit default does not alter existing per‑plan overrides
Bind Enrollment to Generated Bills & Schedule Creation
Given an active autopay enrollment exists for a plan When a post generates bills/installments for that plan Then a scheduled payment is created automatically for each new installment with the installment’s due date And the user receives an immediate confirmation of each schedule And no manual steps are required to create schedules And if a scheduled payment already exists for an installment, no duplicate schedule is created
Self‑Service Pause, Skip One, Resume, and Cancel
Given an active autopay enrollment with at least one upcoming scheduled payment When the user pauses the enrollment Then all unsent future schedules are suspended immediately and no new schedules are created while paused When the user skips the next installment Then only the next upcoming installment is unscheduled, later installments remain unaffected When the user resumes Then scheduling for future installments/bills resumes according to the enrollment settings When the user cancels the enrollment Then the enrollment is terminated immediately and all future unsent schedules are removed And every action (pause, skip, resume, cancel) is recorded in an immutable audit log with timestamp, actor, and reason (if provided)
Consent Artifacts Auditability & Retrieval
Given consent artifacts are stored upon enrollment When an admin or the consenting homeowner requests the consent record for a specific enrollment Then the system returns the exact stored artifacts (timestamp, IP, device identifier, user ID, unit ID, plan ID, funding source ID, mandate/authorization text version, consent method) And the artifacts are read‑only and match the values captured at the time of consent And the record is filterable by member, unit, plan, and date range in reports/export And updates to mandate/authorization text versions do not alter historical artifact records
Payment Method Vaulting (ACH & Card)
"As a member, I want to securely save my bank account or card on file so that autopay can charge the right funding source without me re-entering details."
Description

Integrate a tokenized payment vault that supports bank accounts (ACH) and credit/debit cards, ensuring PCI DSS SAQ A scope with no sensitive data stored on Duesly servers. Provide bank linking via Plaid (or equivalent) with fallback micro-deposit verification, card updater for expiring/reissued cards, and NACHA-compliant account validation. Enable users to add, verify, set default, and remove funding sources; store only masked details (last4, brand, bank name) and mandate references. Enforce method-level rules (e.g., ACH-only plans), handle expirations and invalidations, and surface clear error states. Support soft descriptors, statement descriptors, and routing preferences to minimize fees.

Acceptance Criteria
Tokenized Card Vaulting with Masked Storage
Given a user adds a card via provider-hosted fields or redirect, When submission succeeds, Then only a token and masked metadata (brand, last4, exp MM/YY, funding type, network) are stored on Duesly. Given tokenization is used, Then no PAN or CVV is transmitted through or stored on Duesly servers, and application logs contain no PAN/CVV substrings. Given a successful add, Then the card displays in the wallet as Brand •••• last4, exp MM/YY, and includes a provider reference ID. Given a network or provider error, When add fails, Then the user sees an actionable message and no partial card record is created. Given audit requirements, Then an add event is logged with user, timestamp, provider reference, and no sensitive data.
Bank Account Linking via Plaid with Micro-Deposit Fallback
Given a user selects Bank Account (ACH), When Plaid Link completes, Then Duesly stores a bank token and masked metadata (bank name, last4, account type) and flags the account Verified if instant verification succeeded. Given Plaid is unavailable or declined by the user, When the user opts for manual entry, Then provider-hosted routing/account entry is used and two micro-deposits are initiated within 1 business day. Given micro-deposits are posted, When the user enters both exact amounts, Then the account status changes to Verified; after 10 failed attempts, status becomes Locked and the account cannot be used. Given an ACH account is verified, Then an authorization mandate reference (timestamp, IP, terms version, provider reference) is captured and stored as non-sensitive metadata. Given an ACH account is Unverified or Locked, Then it is not selectable for payments or autopay enrollment.
NACHA Account Validation for ACH Accounts
Given any ACH account is added (Plaid or manual), When NACHA WEB validation runs via the provider, Then the validation result is stored and must be Pass before first debit. Given validation returns Fail or High Risk, Then the account is blocked from use and the user sees a clear message with next steps. Given micro-deposit verification succeeds, Then validation is considered satisfied unless the provider returns an overriding Fail signal. Given audit requirements, Then validation method, timestamp, and reference ID are retained as non-sensitive metadata.
Default Method Selection and ACH-Only Enforcement
Given a user sets a default funding source, When enrolling in autopay, Then that method preselects unless plan rules restrict it. Given a plan marked ACH Only, When a user attempts enrollment with a card default, Then card selection is disabled and the user must select a Verified ACH account to proceed. Given no Verified ACH exists for an ACH Only plan, Then enrollment is blocked and the user is prompted to add and verify ACH. Given community routing preference Prefer ACH to reduce fees is enabled and both methods are allowed, Then a Verified ACH is preferred over card for new enrollments and one-time payments where permitted.
Card Updater for Expiring/Reissued Cards
Given a vaulted card is within 60 days of expiration, When account updater is available, Then exp date and token are refreshed automatically without user input. Given a card is reissued with the same PAN, Then the token is updated and scheduled autopay charges continue without interruption. Given a card is closed or replaced with a new PAN requiring reauthorization, Then the method is marked Invalid, affected enrollments are flagged, and the user is notified to add a new card before the next due date. Given updater actions occur, Then events are logged with outcome, timestamp, and provider reference; no sensitive data is stored.
Funding Source Removal and Invalidation Handling
Given a user removes a funding source, Then it disappears from selection lists immediately and is not used for future charges. Given an existing autopay references a removed method, Then the user is prompted to choose a replacement before the next scheduled debit; autopay pauses if no replacement is set. Given a hard decline or return code indicates the method is invalid (e.g., ACH R02/R03/R05 or card do not honor), Then the method is marked Invalid with reason, becomes unselectable, and an actionable message is shown. Given invalid or expired methods exist, Then UI badges indicate status and attempts to use them are blocked with specific error messages. Given any removal or invalidation action, Then an audit log entry is recorded without exposing sensitive data.
Descriptors and Fee-Optimized Routing Preferences
Given community-level descriptor settings, When a payment is created, Then the soft and statement descriptors sent to the processor match configured values and network limits; overlength values are safely truncated. Given routing preferences, When both ACH and card are eligible, Then transactions route via ACH to minimize fees; otherwise they route via card; the decision and reason are logged. Given descriptor configuration includes invalid characters or exceeds length, Then saving is blocked with field-level validation errors. Given a payment is completed, Then the receipt and admin view display the final descriptor string used for the transaction.
Installment Plan Configuration & Scheduling
"As a treasurer, I want to configure flexible installment schedules for dues so that autopay drafts align with our community’s billing cadence and policies."
Description

Allow treasurers to define installment plan templates with amount, frequency (monthly/quarterly/annual), start/end dates, day-of-month alignment, grace periods, and late fee rules. Support proration for mid-cycle enrollments, catch-up logic for past-due installments, and holiday/weekend handling (pay before/after or next business day). Generate a schedule per enrolled member and automatically create payment intents tied to each bill. Handle changes to plan parameters with forward-only schedule updates and clear member notifications. Ensure idempotent scheduling to prevent duplicate installments and support partial payments and credits application before drafting.

Acceptance Criteria
Plan Template Creation & Validation
Given a treasurer enters amount > 0, selects frequency in {monthly, quarterly, annual}, provides start_date <= end_date, sets day_of_month in {1..28, last}, grace_period_days >= 0, and a late_fee rule (flat|percent) with non-negative value When the treasurer saves the template Then the system validates all fields and rejects invalid inputs with field-level error messages And on success returns a 201 response containing template_id and the persisted parameters And computes and stores the next nominal due date from start_date, frequency, and day_of_month
Schedule Generation, Payment Intents & Idempotency
Given a member enrolls into template T at timestamp X with idempotency key K When enrollment is processed Then an installment schedule is generated from the effective start through end, aligning due dates to T.frequency and T.day_of_month And exactly one payment intent is created per installment and linked to the corresponding bill id And retrying the same enrollment with the same K creates no duplicate installments or intents and returns the original schedule (idempotent) And the response includes the number of installments created and their first/last due dates
Proration for Mid-Cycle Enrollment
Given template amount A for period P and an enrollment that occurs after the period start and before the next aligned due date D When the member is enrolled with proration enabled Then the first installment amount equals round_to_cents(A * remaining_days_in_period / total_days_in_period) And the first installment due date is D And the installment is labeled as prorated with metadata capturing the calculation inputs (A, P, remaining_days, total_days)
Catch-Up Logic for Past-Due Installments
Given a member enrolls into a template where one or more installments are already past due relative to the plan timeline When the schedule is generated Then catch-up installments totaling the unpaid amounts are created And each catch-up installment is set due today with the draft date set to the next eligible processing day And the member notification includes a clear summary of catch-up total and draft dates
Holiday/Weekend Handling Configuration
Given a template has date_adjustment set to one of {pay before, pay after, next business day} and the nominal due date falls on a weekend or configured holiday When the schedule is generated Then the draft date is adjusted according to the selected rule And both the nominal due date and adjusted draft date are stored and visible in the installment details
Forward-Only Plan Changes & Member Notifications
Given a treasurer updates one or more template parameters with an effective change date E When the changes are saved Then only future installments with due_date >= E are recalculated or (re)generated; posted/paid/past-due installments remain unchanged And a new schedule version is recorded with an audit trail of fields changed And each affected member receives a notification summarizing what changed, the effective date, and the updated amounts/dates
Credits & Partial Payments Applied Before Drafting
Given an upcoming installment I has unapplied credits and/or prior partial payments on its associated bill And the draft job for I is about to execute When the draft amount is computed Then credits and partials are applied first, reducing the payment intent amount but not below 0.00 And if the remaining amount is 0.00, the draft is skipped and the intent is voided/canceled with status updated accordingly And the member receipt/notification reflects applied credits and the final drafted amount (or $0.00 with no draft)
Smart Retry & Dunning Engine
"As a treasurer, I want failed autopays to retry intelligently and notify members so that we recover payments without manual chasing or compliance risks."
Description

Implement adaptive retry logic for failed drafts with configurable windows and limits based on failure codes (e.g., ACH R01 insufficient funds vs. R29 unauthorized, card do-not-honor). Space ACH retries by banking days and avoid weekends/holidays; stop retries on hard declines and require re-consent if mandated. Trigger a dunning sequence with multi-channel notices (in-app, email, SMS where enabled), including clear next steps, update funding source prompts, and retry dates. Record all attempts, outcomes, and messages in the audit log; automatically update member/bill status and move accounts to a manual queue after final failure. Prevent duplicate debits and enforce community-level retry policies.

Acceptance Criteria
Adaptive Retry Behavior by Failure Code
Given an autopay draft fails with ACH R01 (Insufficient Funds) When the system schedules retries Then it creates up to the community-configured maximum retry attempts within the configured window, stops after a successful capture or after reaching the limit, and records the next retry date Given an autopay draft fails with ACH R29 or R10 (Unauthorized) or a card decline flagged as a hard decline by the processor configuration When the failure is processed Then the system performs zero further retries, flags the payment method as requiring re-consent or replacement, and initiates the dunning sequence Given an autopay draft fails with a soft, retryable code (e.g., ACH R01, R09, or card soft-decline per gateway mapping) When retries are scheduled Then the system uses the code-specific retry count and spacing defined by the community policy
Banking Days Scheduling for ACH Retries
Given a computed ACH retry date falls on a weekend or US federal banking holiday When the system schedules the next attempt Then the attempt is set to the next available banking day per community timezone Given a retry is scheduled When the next attempt date/time is displayed in notices and UI Then it is shown in the member’s profile timezone, falling back to the community timezone if unavailable Given the community has a daily processing cutoff time When a retry would fall after cutoff Then the retry is scheduled for the next banking day at or after the cutoff
Dunning Notifications: Multi-Channel and Content Requirements
Given a payment attempt fails and dunning is enabled for the community When the system sends notifications Then in-app and email notices are sent, and SMS is sent only if the member has SMS enabled and consent on file Given a dunning notice is sent When the member receives it Then the message includes the bill identifier, amount due, failure reason in plain language, the next retry date/time, and a secure link to update the funding source Given the next retry date changes (e.g., due to holiday adjustment or member action) When subsequent dunning notices are sent Then the updated retry date/time is shown consistently across all channels
Comprehensive Audit Logging of Attempts and Communications
Given any payment attempt (initial or retry) occurs When the attempt completes (success or failure) Then an audit log entry is written with timestamp (UTC), actor, bill ID, member ID, payment method fingerprint, amount, attempt number, outcome, processor reference, and failure code/description (if any) Given any dunning communication is sent When the message is dispatched Then the audit log records the channel, template/version, message ID, recipient, and delivery status Given audit logs are stored When a Treasurer views the bill’s activity Then all attempts and notices are visible in chronological order and are immutable
Duplicate Debit Prevention and Idempotency
Given multiple triggers attempt to create a debit for the same bill installment concurrently When the debit is submitted to the processor Then an idempotency key scoped to bill-installment-payment-method prevents more than one debit from being created Given a provider network retry occurs or a webhook is replayed When the event is processed Then the system recognizes the existing attempt by idempotency/reference and does not create a duplicate charge, logging the duplicate-prevented outcome Given a retry is already scheduled for an installment When an additional retry would be enqueued outside policy Then it is not scheduled and the decision is logged
Automatic Status Updates and Manual Queue Escalation
Given a retry attempt succeeds When the payment is captured Then the bill/installment status is updated to Paid, member ledger reflects the payment, all pending retries are canceled, and dunning is stopped Given the final allowed retry attempt fails per policy When the outcome is recorded Then the bill/installment status changes to Final Failed, the member/account is marked Needs Attention, the case moves to the Manual Collections queue, and Treasurer(s) are notified Given an installment is in Manual Collections When a Treasurer records a manual resolution or the member updates a valid funding source Then future autopay for subsequent installments can resume per policy without reattempting the final-failed installment
Community-Level Policy Enforcement and Permissions
Given a community-specific retry and dunning policy is configured (max attempts per code, spacing, cadence) When a failure is processed for a member of that community Then the configured policy is enforced; if absent, the platform default policy is applied Given a retry would exceed the policy’s maximum attempts or window When scheduling logic runs Then no further retries are scheduled and the reason is recorded in the audit log Given a user without the required role attempts to override retry settings via UI or API When the request is made Then the system denies the change with a permission error and logs the attempt
Instant Receipts & Payment Notifications
"As a homeowner, I want immediate confirmation and receipts for autopay transactions so that I know what was paid and can keep records."
Description

Send instant receipts and status updates for autopay events: scheduled, initiated, authorized, failed, retried, and settled. Provide channel preferences per user and default community settings; include plan name, installment number, amount, method last4, and expected settlement date. For ACH, emit a pending receipt on initiation and a final receipt on settlement; for cards, confirm immediately on capture. Offer a receipts center in the feed with searchable history and download as PDF for records. Support localization, resend, and read/unread tracking to lift visibility and member confidence.

Acceptance Criteria
Emit Notifications for Autopay Lifecycle Events
Given a member is enrolled in Plan Autopay with an active payment method and a scheduled installment When the payment lifecycle progresses through scheduled, initiated, authorized, failed, retried, or settled Then the system creates exactly one corresponding receipt/notification in the member’s feed for each distinct event And the notification is dispatched via the member’s enabled channels per preference And duplicate callbacks for the same event do not create additional receipts/notifications
Receipt Payload Includes Required Fields
Given any autopay receipt or status notification is generated Then the receipt includes plan name, installment number, amount, payment method type and last4, expected settlement date (if applicable), and the event status label And the values reflect the installment and method as of the event time
Channel Preferences with Community Default Fallback
Given community default notification settings and a member’s personal channel preferences When a receipt or status update is generated Then delivery uses the member’s preferences; if none are set, the community default is used And an in-app feed receipt is always created regardless of channel preferences
ACH Pending and Final Receipts vs Card Immediate Confirmation
Given a member’s autopay installment will be paid via ACH When the payment is initiated Then a pending receipt is created and sent with the expected settlement date And when the payment settles successfully Then a final settled receipt is created and sent And if the settlement fails Then a failed receipt is created and sent Given a member’s autopay installment will be paid via card When the payment is captured Then a confirmation receipt is created and sent immediately And no additional settlement receipt is sent for the same installment
Receipts Center: Search, Filter, Read/Unread, and PDF Download
Given a member opens the Receipts Center in the feed When they search by keyword or filter by date range, status, plan name, installment number, or amount range Then matching receipts are returned with read/unread indicators And selecting a receipt opens details with a Download PDF action that produces a PDF matching the displayed content and locale
Resend Receipt from Receipt Details
Given a member or treasurer is viewing a receipt’s details When they select Resend and choose one or more allowed delivery channels Then the system resends the receipt with identical content And records the resend timestamp and channels in the receipt’s history And the resent notification appears unread to the recipient until viewed
Localized Receipts with Fallback
Given a user has a preferred locale or inherits the community default locale When any receipt is rendered in-app, sent via email, or exported as PDF Then static text, currency, numbers, and date/time formats use the effective locale And if the user’s locale is unsupported, the community default is used; otherwise the platform default locale is used
Treasurer Forecasting & Reconciliation
"As a treasurer, I want to forecast and reconcile autopay cash flows so that I can plan budgets and quickly resolve exceptions."
Description

Provide a forecasting view that aggregates upcoming autopay drafts by date and plan, projecting cash flow for the next 30/60/90 days. Show expected vs. actual settlements, deposit batches with trace IDs, and variance explanations (skips, retries, failures). Enable CSV export and accounting integrations (e.g., QuickBooks Online) with mapping to chart of accounts and classes. Surface a prioritized queue for failed/exception items and bulk actions (send reminder, adjust late fee, write note). Include filters by community, building, and plan, and support webhooks for settlement events to sync external systems.

Acceptance Criteria
30/60/90-Day Cash Flow Forecast by Date and Plan
- Given scheduled autopay drafts across multiple plans and communities, When the Treasurer selects 30, 60, or 90 days in Forecasting, Then totals are displayed per date and per plan and equal the sum of included drafts. - Given a community timezone is configured, When draft dates are aggregated, Then all calculations use that timezone and match component transaction dates. - Given drafts are paused or canceled before their draft date, When the forecast is generated, Then those drafts are excluded and an exclusions count is shown. - Given plan/community/building filters are applied, When the forecast refreshes, Then totals update within 1 second for datasets up to 10,000 rows and reflect only the filtered scope. - Given data is visible on-screen, When the Treasurer hovers a date total, Then a tooltip shows count of drafts and subtotal by plan.
Settlement Reconciliation with Variance Explanations
- Given expected drafts for a date, When settlements are posted, Then the Reconciliation view shows Expected, Actual, and Delta amounts per date and per plan with a variance percent. - Given a deposit batch is created, When settlement completes, Then each deposit row displays processor batch ID and bank trace ID and links to detail. - Given variances occur (skips, retries, failures), When a date group is expanded, Then each variance lists member, amount, reason code, and current retry state. - Given settlement webhooks are processed, When data changes, Then the view updates within 5 minutes and shows a Last Updated timestamp. - Given role-based access controls, When a non‑Treasurer attempts access, Then the view is denied with an authorization message.
Failed/Exception Items Queue with Bulk Actions
- Given failed, skipped, or exception drafts exist, When the Exceptions queue loads, Then items are prioritized by next retry datetime ascending (tie-breaker: amount descending) with reason badges (NSF, auth_failed, account_closed, limit_exceeded). - Given multiple items are selected, When Send Reminder is executed, Then messages are sent per recipient with rate limits (max 1/day/recipient) and each delivery is logged with timestamp and channel. - Given late fees are enabled, When Adjust Late Fee is applied in bulk, Then fee journal entries are created per member and reflected in ledgers and next statements. - Given a user adds a note, When saved, Then the note is timestamped, attributed, non-editable, and appears in both the queue item and member timeline. - Given a retry schedule exists, When the Treasurer overrides the next retry time, Then the schedule updates, the forecast reflects the change, and an audit log entry is created.
Cross-Section Filters for Forecast & Reconciliation
- Given filters for community, building, and plan, When any combination is applied, Then both Forecast and Reconciliation reflect the intersection and display updated totals. - Given filters are set, When navigating between Forecast and Reconciliation, Then selections persist for the session and are applied to exports. - Given no records match the filters, When results render, Then an empty state is shown with zero totals and no errors. - Given datasets up to 100,000 transactions, When filters change, Then results render within 2 seconds in 95% of cases.
CSV Export and QuickBooks Online Sync with Account/Class Mapping
- Given plan-to-account and class mappings are configured, When the Treasurer saves mappings, Then the system validates accounts/classes exist in QBO and prevents duplicate or circular mappings. - Given a filtered view, When CSV Export is requested, Then the file contains date, plan, member, amount, status, variance reason, batch ID, and trace ID columns and matches on-screen totals. - Given QBO is connected, When Sync Deposits runs for a date range, Then deposits are created with line items mapped per plan/class and processor fees posted to the designated expense account. - Given idempotency keys per batch, When the same range is synced again, Then no duplicate entries are created in QBO and a no-op is logged. - Given QBO returns errors, When sync executes, Then the job reports actionable errors, skips failed records, and supports retry of only failed items.
Settlement Event Webhooks for External Sync
- Given a webhook URL and shared secret are registered, When settlement events occur (payment_succeeded, payment_failed, retry_scheduled, deposit_created, deposit_settled), Then Duesly POSTs JSON payloads including event_id, type, timestamps, amounts, member_id, plan_id, batch_id, and trace_id. - Given signed webhooks, When the receiver verifies, Then the signature is HMAC-SHA256 over the raw payload with the shared secret and includes a timestamp header to prevent replay beyond 5 minutes. - Given a non-2xx response, When deliveries are retried, Then exponential backoff is applied over 24 hours up to 10 attempts and at-least-once delivery is guaranteed. - Given webhook activity, When viewing the Webhooks dashboard, Then each attempt shows status code, latency, last error, and supports replay for events from the last 30 days.
Security, Compliance & Audit Logging
"As a compliance officer, I want auditable, secure handling of autopay data and consents so that the organization meets regulatory obligations and protects members’ information."
Description

Enforce NACHA, Reg E, and card network compliance for recurring payments, including clear authorization language, revocation handling, and notification windows. Implement end-to-end encryption in transit and at rest, robust key management, role-based access controls, and least-privilege service boundaries. Maintain immutable audit logs for enrollments, consents, schedule changes, retries, notices, and user actions with time, actor, and context. Define data retention and deletion policies for PII and payment artifacts; rate-limit sensitive endpoints and add anomaly detection for fraud and account takeover. Provide disaster recovery objectives (RPO/RTO) and incident response playbooks specific to payment failures and data breaches.

Acceptance Criteria
Compliant Autopay Authorization Capture (ACH/Card)
Given a member enrolls in Plan Autopay via ACH or card When the enrollment form is displayed Then the authorization text must include frequency, start date, amount or amount source, statement descriptor/Company Name and ACH Company ID (for ACH), revocation instructions, and support contact, with the consent checkbox unchecked by default Given the member submits the enrollment When consent is collected Then the system must require an explicit checkbox, typed name (or e-signature), and capture timestamp (UTC), IP address, and user agent And a consent artifact (PDF/HTML render with a content hash) is stored as write-once and linked to the enrollment And a confirmation receipt with the full authorization text is sent to the member within 1 minute And an audit log entry "autopay_authorized" is recorded with actor, time, and context And the enrollment is rejected if any required consent element is missing
Autopay Revocation and Stop-Payment Handling
Given an enrolled member initiates autopay revocation via UI or verified support channel When the revocation request is submitted Then the system records revocation with timestamp (UTC), channel, and actor, and sends immediate confirmation to the member And all future debits are disabled And if the revocation occurs ≥ 3 business days before the next scheduled debit (cutoff 5:00 PM local time), the next debit is not submitted to the processor And if within 3 business days, the system attempts cancellation if not yet submitted and informs the member of the outcome And an audit log entry "autopay_revoked" is appended with full context And a revocation artifact (rendered confirmation with hash) is stored immutably
Pre‑Debit and Schedule Change Notifications
Rule (Fixed-amount recurring): On enrollment, send schedule confirmation with amount, date, frequency, and change/revocation instructions; for any change to amount or date, send change notice ≥ 7 calendar days before effective date Rule (Variable-amount recurring): Send pre-debit notice ≥ 10 calendar days before each debit including the exact amount, debit date, and revocation instructions Rule (Retries): For failed debits, notify the member within 1 hour of failure with return reason and planned retry date; send retry notice ≥ 2 business days before retry Rule (Delivery and logging): Track delivery status (success/failure) per channel; failed deliveries trigger a fallback channel within 15 minutes; all notices are audit-logged with content summary and delivery outcome
Access Security: RBAC, Least Privilege, and Abuse Protections
Rule (RBAC/Least privilege): Define roles (Owner, Treasurer, Manager, Auditor, Support) with a default-deny policy; only Treasurer/Manager can view full PII/payment artifacts; Auditor has read-only audit log access; Support sees masked PII; Owners see only their own data Rule (Policy enforcement): All access decisions go through a centralized policy engine; unauthorized requests return 403 and are audit-logged with reason Rule (Sensitive endpoints): Identify and protect login, MFA, password reset, payment method add/update, bank verification, autopay enroll/revoke, schedule change, and audit export endpoints Rule (Rate limiting): Enforce per-IP and per-account limits (≥ 10 requests/min and ≥ 100/hour thresholds) with exponential backoff; after 5 consecutive failures for an auth-sensitive action, lock the action for 15 minutes and require CAPTCHA or MFA step-up Rule (Anomaly detection): Flag high-risk sequences (e.g., login from new location/device followed by payment method change within 10 minutes); require step-up verification and hold the action until verification completes Rule (Monitoring): Security alerts for rate-limit blocks and anomalies must appear on the security dashboard within 2 minutes and page on-call for critical severities
Encryption in Transit/At Rest and Key Management
Rule (Transport): All external connections enforce TLS 1.2+ with HSTS and PFS; weak ciphers are disabled; automated tests verify cipher suites and cert validity Rule (At rest): All sensitive data is encrypted at rest using AES-256 (or FIPS 140-2 validated equivalent); PANs are never stored—only network tokens/last4/brand/exp; bank account numbers are tokenized or encrypted with field-level keys Rule (Key management): Keys are managed in KMS/HSM; access limited to service principals; access is fully logged; keys are rotated at least annually or upon compromise Rule (Secrets): Application secrets are stored in a secrets manager; no secrets are hard-coded; rotation occurs at least every 90 days Rule (Verification): CI/CD includes checks for TLS configuration and data scans that detect prohibited PAN/ABA patterns; a penetration test reports no critical findings related to crypto or key management
Immutable Audit Logging with Retention/Deletion Policies
Rule (Coverage): Enrollment, consent, schedule changes, debit submissions, returns, retries, notices, revocations, access grants/denials, and administrative actions are all audit-logged Rule (Record schema): Each entry contains event_type, actor_id/role (or system), subject_id, timestamp (UTC ISO 8601), request_id, IP, user_agent, and before/after summaries where applicable Rule (Immutability): Logs are append-only and tamper-evident via hash chaining or WORM storage; verification tooling detects any alteration Rule (Access): Only the Auditor role can read full audit logs; all reads are themselves audit-logged Rule (Retention): Audit logs are retained 7 years; PII is retained only as necessary and deleted/redacted within 30 days of a verified deletion request unless on legal hold; payment artifacts (tokens/mandates) are deleted within 90 days of autopay cancellation or final settlement, whichever is later Rule (Execution): A scheduled job enforces retention policies; deletions produce a machine-verifiable deletion certificate and corresponding audit entries
Disaster Recovery and Incident Response for Payment Events
Rule (Objectives): Autopay services define and publish RPO ≤ 15 minutes and RTO ≤ 2 hours Rule (Backups/Failover): Nightly cross-region backups and point-in-time recovery are enabled; quarterly DR tests demonstrate meeting RPO/RTO and produce a report Rule (ACH retry compliance): Automatic retries for ACH failures do not exceed 2 retries and are spaced at least 3 business days apart; members receive failure and retry schedule notifications within 1 hour of the event Rule (Incident response): Playbooks exist for processor outage, mass return event, and data breach; targets: detection < 15 minutes, containment < 60 minutes; customer/regulator notification templates are prepared Rule (Post-incident): Every incident has a timeline, root cause analysis, and remediation tasks tracked to completion; a postmortem is published within 5 business days Rule (Logging): All DR tests and incidents are audit-logged with outcomes and sign-offs

Smart Reflow

When a payment is early, partial, or missed, the plan recalculates remaining installments within your guardrails—extending the end date or adjusting amounts as configured. Members see the updated schedule immediately, and the ledger logs every change for audit clarity. Fewer manual edits, less confusion, steadier recoveries.

Requirements

Guardrail Configuration Panel
"As a board treasurer, I want to set guardrails for how plans can flex so that reflows stay fair, predictable, and compliant with our community policies."
Description

Provide an admin-configurable set of guardrails that dictate how Smart Reflow may adjust payment plans, including minimum and maximum installment amounts, maximum plan extension length, rounding rules, grace periods for missed installments, early-payment application order (principal, fees, interest), treatment of late fees and interest, weekend/holiday due-date shift rules, minimum partial payment thresholds, and a cap on installment count. Settings are available under Billing > Plans, can be set per plan template and overridden per member plan, and are versioned. Server-side validations prevent contradictory configurations and present inline guidance. Defaults are provided for quick setup. Any guardrail changes only affect future recalculations and every change is logged for audit.

Acceptance Criteria
Guardrail Panel Access and Field Availability
Given an admin user with permission to edit plans When they navigate to Billing > Plans and open the Guardrail Configuration panel for a plan template Then the panel displays editable controls for: minimum installment amount, maximum installment amount, cap on installment count, maximum plan extension length, rounding rule (mode and increment), grace period for missed installments (days), early-payment application order (principal, fees, interest), treatment of late fees, treatment of interest, weekend/holiday due-date shift rule, minimum partial payment threshold And each control shows inline help text and unit/format indicators And the panel is not visible to non-admin users
Template Defaults and Save/Persist Behavior
Given a new plan template is being created When the Guardrail Configuration panel loads Then all guardrail fields are pre-populated with system defaults And when the admin saves the template without changes Then the defaults are persisted and shown on subsequent edit And when the admin updates one or more fields and clicks Save Then the updated values are persisted and applied to newly created member plans using this template
Member Plan Overrides and Precedence
Given a member plan created from a template with guardrails When an admin opens the member plan’s Guardrail Overrides Then each guardrail can be toggled to 'Use template' or 'Override' And when an override value is saved for a guardrail Then Smart Reflow uses the member override for that guardrail and template values for others And when the override is cleared Then Smart Reflow reverts to using the template value
Server-side Validation and Inline Guidance for Contradictory Settings
Given the admin enters a minimum installment amount greater than the maximum When Save is clicked Then the save is rejected with HTTP 422 And inline errors display on both fields: "Minimum must be less than or equal to Maximum" Given the admin sets an installment count cap below the current remaining installments on a member plan When Save is clicked Then the save is rejected with inline error: "Cap cannot be less than remaining installments (N)" Given a rounding increment not compatible with the plan currency minor unit is entered When Save is clicked Then the save is rejected with inline error: "Rounding increment must be a multiple of currency minor unit" Given an early-payment application order contains duplicates or is missing an element When Save is clicked Then the save is rejected with inline error: "Order must include each of principal, fees, interest exactly once"
Versioning and Future-only Impact of Guardrail Changes
Given a plan template with guardrail version v1 applied to member plans When the admin changes any guardrail on the template and saves Then a new version v2 is created with version ID, editor, timestamp, and change summary And v2 is used only for recalculations that occur after the save time And past schedules and recalculations continue to reference v1 And a versions history view lists all prior versions with diff summaries
Audit Logging of Guardrail Edits
Given an admin edits guardrails at the template or member plan scope When the save succeeds Then an audit log entry is created capturing scope, before/after values, actor, timestamp, IP, and optional rationale And the entry is immutable and filterable by plan, member, field, and date range And the ledger of the affected plan references the guardrail version used for each recalculation event
Weekend and Holiday Due-date Shift Rules in Reflow
Given the weekend/holiday shift rule is set to "Next business day" and the holiday calendar includes 2025-12-25 When a recalculation produces an installment due date that falls on a weekend or on 2025-12-25 Then the due date is shifted to the next non-weekend, non-holiday business day And the new due date is displayed to the member immediately in the schedule And the previous date and the applied rule are recorded in the audit log
Real-time Recalculation Engine
"As a property manager, I want the system to automatically recalculate plans when payments don’t match the schedule so that I don’t have to make manual edits."
Description

Implement a deterministic, idempotent service that recalculates remaining installments immediately upon payment events (early, partial, missed, failed) using the active guardrails and plan policy (fixed-term vs fixed-amount). The engine adjusts either installment amounts or plan end date per configuration, preserves total owed minus payments/waivers, reconciles rounding on the final installment, supports time zone cutoffs, and is concurrency-safe. It is triggered by gateway webhooks, manual postings, or scheduled sweeps, and returns a structured result containing the new schedule and a computed delta for UI and ledger consumption. Performance target is sub-300ms p95 with robust error handling and safe fallback (no schedule change) plus alerting.

Acceptance Criteria
Early Payment — Fixed-Term, Adjust Amounts
Given a fixed-term installment plan with policy=adjust_amounts and active guardrails And a future installment is paid early in full via manual posting or webhook When the recalculation engine is triggered Then it preserves total_owed_minus(payments+waivers) And keeps the original plan end_date unchanged And redistributes remaining amounts across remaining installments within guardrails And applies rounding to the final installment so the sum equals the remaining balance with absolute rounding error <= 1 minor unit And returns a structured result containing new_schedule and computed_delta And repeating the recalculation with the same inputs yields the same new_schedule and delta (deterministic)
Partial Payment — Fixed-Amount, Extend End Date
Given a fixed-amount installment plan with policy=extend_end_date and a configured per_installment_amount And the current installment receives a partial payment (amount < installment_amount) When the recalculation engine is triggered Then it keeps installment amounts fixed at the configured per_installment_amount And adds the minimum number of additional installments required to cover the remaining balance within guardrails And sets any final installment to the exact residual after rounding to currency minor units And preserves total_owed_minus(payments+waivers) And updates the plan end_date accordingly without exceeding guardrails on maximum extension And returns a structured result with computed_delta listing added_installments and any adjusted final installment
Missed/Failed Installment at Time-Zone Cutoff
Given a community-configured time zone and a daily cutoff_time for installment status And an installment is unpaid at cutoff_time or a payment failure event is received When a scheduled sweep or webhook triggers recalculation Then the installment is marked missed or failed according to policy and guardrails And the remaining schedule is recomputed per plan policy (adjust amounts or extend end date) And no installment date is moved before the current business day in the configured time zone And the structured result includes reason_code (missed or failed), previous_schedule_snapshot, and new_schedule And an audit log entry is created with trigger_source, cutoff_timestamp_local, and actor=system
Idempotent Processing of Duplicate Payment Events
Given a payment event with a stable identifier (e.g., payment_id and event_type) When the same event is delivered multiple times Then the engine performs at most one schedule mutation And subsequent deliveries return the same new_schedule and computed_delta with no additional mutations And exactly one ledger/audit entry exists for the recalculation
Concurrency Safety Under Simultaneous Triggers
Given two or more triggers (e.g., webhook and scheduled sweep) target the same plan concurrently When the recalculation engine processes them Then exactly one update succeeds per plan_version and writes the mutation And conflicting attempts receive a retryable concurrency_conflict without mutating the schedule And the final schedule equals that produced by serially applying the same events in timestamp order And no ledger entry is created for conflicted attempts; one entry exists for the winning mutation
Structured Result and Audit Trail Completeness
Given any successful recalculation When the engine returns its response Then it includes new_schedule (installment_id, date, amount, status) and computed_delta (changed/added/removed installments; totals_before/after; end_date_before/after) And an immutable ledger entry is written capturing previous_schedule_snapshot, new_schedule, computed_delta, actor, timestamp, and trigger_source And the ledger entry can be retrieved by plan_id and correlation_id and matches the response payload
Performance, Error Handling, and Safe Fallback
Given normal operating conditions across recent recalculation calls When measuring latency at the engine boundary Then p95 latency is <= 300ms And no successful response contains partial mutations or validation errors And on any internal error or validation failure, the engine performs no schedule changes, returns a failure with unchanged schedule_version, emits an alert with correlation_id and error_class, and records an audit entry with outcome=failure And retries do not create duplicate ledger entries or duplicate mutations
Member Schedule Auto-Update
"As a homeowner, I want to see my updated payment schedule right away so that I know exactly what to pay next."
Description

Update the member-facing schedule and feed card instantly after a reflow to reflect new installment amounts and due dates, with a clear change summary (for example, count of adjusted installments and new end date) and a visual highlight for recent changes. Ensure parity across web and mobile, real-time cache invalidation via push or websockets, and accessibility compliance. Provide a detail view that shows the updated schedule alongside a read-only snapshot of the previous schedule for transparency, without requiring a page reload.

Acceptance Criteria
Instant Schedule and Feed Card Refresh
Given a member is viewing their schedule or feed card on web or mobile And a reflow completes for that member’s plan When the reflow event is received by the client Then the visible schedule and feed card update without requiring a manual reload And the update completes within 2 seconds for 95% of events and within 5 seconds for 99% of events And all installment amounts and due dates exactly match the reflowed plan from the event payload And no duplicate or missing installments are rendered And installments remain in chronological order And any loading indicator is cleared once data is current
Change Summary Content
Given a reflow has updated the schedule When the new schedule renders Then a change summary is shown above the schedule and in the feed card And it displays the count of adjusted installments And it displays the new plan end date or the text "End date unchanged" if applicable And it includes the reflow effective timestamp in the member’s local timezone And the summary text does not exceed 180 characters And the summary can be dismissed and auto-hides after 24 hours if not dismissed
Visual Highlight of Updated Installments
Given installments were adjusted by the reflow When the schedule is updated Then each changed installment row is visually highlighted And the highlight includes a non-color-only indicator (e.g., icon/badge) in addition to color And unchanged installments are not highlighted And the highlight persists until the summary is dismissed or 24 hours elapse And activating the highlight affordance reveals the text "Updated by Smart Reflow"
Realtime Delivery and Cache Invalidation
Given the client is connected to a realtime channel (WebSocket or push) When the backend publishes a reflow event for a member Then the client invalidates cached schedule/feed data for that plan and fetches the latest data atomically And only one refresh is triggered per unique event (no duplicate fetches) And if the realtime channel is unavailable, the client falls back to polling and updates within 15 seconds And after the update completes, stale content is not shown in the schedule or feed card
Accessible Dynamic Update
Given a dynamic schedule update occurs When the new data is rendered Then a polite live region announces: "Your payment schedule was updated. {X} installments changed. New end date: {date}." And keyboard focus remains on the previously focused element (no unexpected focus shift) And visual highlights meet a minimum 4.5:1 contrast ratio and include a text/icon cue (not color alone) And all interactions remain operable via keyboard only (no pointer-specific requirements) And screen reader users can navigate to the previous-schedule snapshot via a labeled landmark or heading
Detail View with Previous Schedule Snapshot
Given a member opens the schedule detail view after a reflow When the view loads Then the updated schedule is shown alongside a read-only snapshot of the previous schedule without a full page reload And the snapshot is labeled with the reflow effective timestamp And amounts, due dates, and installment count in the snapshot match the state immediately prior to the reflow And differences between old and new are indicated per installment And the snapshot content cannot be edited or deleted by the member
Cross-Platform Parity
Given the same reflow event When viewed on web, iOS, and Android Then schedule values, installment order, change summary content, highlight behavior, and detail view data are identical across platforms And localized date and currency formats are consistent per device locale settings And the update latency target (<= 2 seconds for 95% of events) is met on each platform
Ledger Diff and Audit Trail
"As a board secretary, I want a clear audit trail of schedule changes so that we can demonstrate transparency and accuracy during reviews."
Description

Record each reflow as an immutable ledger entry containing the triggering event (payment ID, miss, waiver), guardrails version, actor (system or user), timestamp, plan totals, and a structured before-and-after schedule diff at the installment level. Expose a queryable, exportable history per plan (CSV/PDF) and integrate entries into Duesly’s compliance feed with role-based access controls to protect sensitive financial data. Provide APIs for back-office reporting and external audit verification.

Acceptance Criteria
Ledger entry on reflow creation
Given an existing payment plan with guardrails version G and at least one scheduled installment When a reflow is triggered by an event of type early, partial, missed, waiver, manual_adjustment, or system_reflow Then exactly one immutable ledger entry is persisted within 1 second of the trigger with required fields: entry_id (UUIDv4), plan_id, trigger_type, trigger_reference_id (nullable), guardrails_version=G, actor (system or user_id), timestamp (ISO 8601 UTC), plan_totals_before (total_scheduled, total_paid, remaining_due), plan_totals_after (total_scheduled, total_paid, remaining_due), and schedule_diff And the entry is retrievable by plan_id and entry_id And the persisted entry is readable via UI and API with identical field values
Structured before-and-after schedule diff
Given a plan reflow where zero or more installments are changed, added, or removed When the ledger entry is created Then schedule_diff lists only changed installments, each with: installment_id (nullable for added), op ∈ {added, removed, updated}, due_date_before/after, amount_before/after, status_before/after, sequence_before/after And schedule_diff entries are deterministically ordered by sequence_before then sequence_after And the sum of amounts in the post-reflow schedule equals plan_totals_after.total_scheduled within ±$0.01 And each updated installment in the diff reflects at least one changed field among due_date, amount, or status
Queryable and exportable plan history
Given a plan with at least 1000 ledger entries across multiple trigger types and actors When a user with role Manager filters history by date range, actor, trigger_type, and guardrails_version Then the system returns matching entries within 2 seconds for up to 10,000 results, paginated with a page size of 50 using cursor-based pagination And the user can export the filtered results to CSV and PDF And the exported files include columns: entry_id, plan_id, timestamp_utc, actor, trigger_type, trigger_reference_id, guardrails_version, plan_totals_before.total_scheduled, plan_totals_before.total_paid, plan_totals_before.remaining_due, plan_totals_after.total_scheduled, plan_totals_after.total_paid, plan_totals_after.remaining_due And CSV is UTF-8, comma-delimited, RFC 4180 compliant; PDF contains a summary header and per-entry details And the export filename pattern is duesly_plan_<plan_id>_ledger_<yyyy-mm-dd>_<hhmmssZ>.<ext> And the exported row count and first/last entry_id match the filtered result set And an audit log entry records requester, filters, timestamp, and file checksum
Compliance feed integration with RBAC
Given ledger entries exist for multiple plans with sensitive financial details When the compliance feed renders entries Then users with roles Board Admin, Manager, or Auditor see full entry details; Members only see entries for their own plan with monetary amounts redacted to ranges And unauthorized users receive HTTP 403 with no data leakage via counts, messages, or timing And each feed item links to the source ledger entry only if the viewer is authorized And every access is logged with user_id, role, resource_id, action, timestamp, and outcome And permission changes propagate to the feed within 60 seconds
External audit and back-office APIs
Given an OAuth2 client with scope ledger.read for an organization When it calls GET /api/v1/plans/{plan_id}/ledger-entries with filters (date_from, date_to, actor, trigger_type, guardrails_version) and pagination (limit<=500, cursor) Then the API responds 200 within 1 second p95 with a stable JSON response containing: items[], next_cursor, total_estimate And each item includes required fields plus content_hash and previous_hash And GET /api/v1/ledger-entries/{entry_id} returns the exact entry payload by id And responses include ETag and Cache-Control: public, max-age=60; requests over 600/minute return 429 with Retry-After And the API schema is published via OpenAPI and passes contract tests; breaking changes require a new versioned path
Immutability and append-only model
Given a previously created ledger entry When any client attempts to modify or delete it via PUT, PATCH, or DELETE Then the system returns 405 Method Not Allowed (or 409 Conflict for semantic updates) and the stored entry remains unchanged And each entry stores content_hash (SHA-256 of canonical JSON) and previous_hash to form a verifiable chain per plan And a nightly process verifies chain integrity for 100% of plans and raises an alert on any break And corrections are represented as new entries with correction_of=<entry_id>, leaving the original entry intact and linking both in queries and exports
Performance and reliability of reflow logging
Given a reflow is triggered during peak usage When the ledger entry is written as part of the reflow Then reflow completion time increases by no more than 200 ms at p95 due to logging And ledger writes are at-least-once and idempotent: retries do not create duplicates, enforced by an idempotency_key derived from plan_id + trigger_reference_id + time bucket And if the primary store is unavailable, the event is queued and persisted within 5 minutes; the UI shows a temporary "pending audit trail" badge until persisted And monitoring captures success/failure counts, latency p50/p95/p99, and queue depth with alerts on SLO breaches
Admin Preview and Override
"As a community manager, I want to preview and adjust reflows so that edge cases can be handled without breaking policy."
Description

Enable managers to simulate a reflow before applying it, previewing proposed schedules and deltas, then accept, adjust within guardrails, or reject with a reason. Provide controlled bypass with elevated permissions and mandatory justification. Support one-click rollback to any prior schedule version recorded in the ledger. Include bulk actions to apply consistent overrides to multiple plans after systemic issues. Every action is logged with reason codes and user attribution, and permissions are enforced per role.

Acceptance Criteria
Simulate Reflow Preview
Given a manager with "Manage Payment Plans" permission views a plan with an early, partial, or missed payment When they click "Simulate Reflow" Then a preview modal renders within 2 seconds for plans with ≤60 remaining installments And the preview shows for each remaining installment: due date, amount, and delta vs current schedule And the preview shows updated plan end date, total remaining amount, and any guardrail warnings And no changes are persisted until "Apply", "Adjust", or "Reject" is confirmed
Adjust Within Guardrails and Apply
Given a reflow preview is open And the user has "Manage Payment Plans" permission When the user adjusts installment amounts and/or end date within configured guardrails Then the UI validates inputs in real time and displays recalculated totals and end date And clicking "Apply" writes a new schedule version to the ledger with reason code "Admin Adjust Within Guardrails", user attribution, timestamp, and version number incremented And the member's schedule view updates within 5 seconds And a success confirmation is shown
Reject Reflow With Reason
Given a reflow preview is open When the user clicks "Reject" Then the system requires selection of a standardized reason code and allows an optional note up to 500 characters And no schedule changes are persisted And the ledger records a "Reflow Rejected" entry with reason code, note (if provided), user attribution, and timestamp
Elevated Bypass of Guardrails
Given a user without "Override Guardrails" tries to save adjustments that violate guardrails When they click "Apply" Then the save is blocked with a clear error listing violated guardrails Given a user with "Override Guardrails" permission makes the same adjustments When they click "Apply" Then the system requires a mandatory justification of at least 15 characters and a reason code And upon confirmation, the schedule is saved and the ledger marks the entry as "Guardrail Bypass" with justification, user role, and permission ID
One-Click Rollback to Prior Version
Given a plan has 2 or more schedule versions in the ledger When the user opens "Version History" and clicks "Rollback" on a prior version Then a confirmation shows a summary of changes (installment count, total, end date) And upon confirm, the current schedule is replaced exactly by the selected version, a new version entry is created referencing the source version, and the member view updates within 5 seconds And any payments already collected remain allocated and are not duplicated And the pre-rollback version remains in the history
Bulk Overrides After Systemic Issue
Given the user selects 2–500 plans via filters and checkboxes When they apply a bulk override template (e.g., extend end date by 1 month, cap per-installment increase at $X) Then the system validates eligibility per plan, shows counts of included/excluded with reasons, and requires a reason code And applying the bulk action executes atomically per plan with a bulk operation ID, produces a per-plan success/failure report, and logs each change with user attribution And the operation is idempotent using a client-supplied idempotency key; duplicate submissions do not create additional versions And 95% of batches up to 500 plans complete within 60 seconds
Reminder and Autopay Alignment
"As a resident on autopay, I want my reminders and drafts to follow the updated schedule so that I don’t miss payments or get overcharged."
Description

Automatically reschedule existing reminders and autopay instructions to match the reflowed schedule, canceling superseded reminders and issuing a single consolidated update notification that summarizes changes. Respect communication throttling, localization, and templating standards. For autopay amounts that increase beyond a configured threshold, obtain and record renewed member consent before the next draft. Log all communications and schedule updates for traceability.

Acceptance Criteria
Auto-Reschedule Reminders After Reflow
Given a member has an active installment plan with scheduled reminders and autopay configured And a reflow recalculates remaining installments When the reflow job completes Then all future reminders are rescheduled to align with the new due dates and original cadence windows And all reminders tied to superseded dates are canceled And exactly one consolidated update notification is sent to the member per reflow within the configured dispatch window And no pending reminder remains whose due date/time does not match the current schedule
Communication Throttling, Debounce, and Consolidation
Given a communication throttle policy is configured for the community/member And a reflow triggers reminder cancellations and schedule updates When preparing outbound notifications Then the consolidated update respects throttle limits (delay/queue if limit reached) and records the deferral reason and next send time And superseded reminders are not sent And multiple reflows within the configured debounce window result in a single consolidated update that reflects the latest schedule only And no more than one consolidated update is sent per member per reflow event
Localization and Templating Compliance for Reflow Updates
Given the member’s locale, time zone, and currency are known When the consolidated update and any rescheduled reminders are generated Then all dates/times are rendered in the member’s time zone And amounts are formatted per locale/currency And the correct localized template version is used; if missing, the default locale template is used and the fallback is logged And the template ID, locale, and version are recorded with the notification log
Autopay Increase Requires Renewed Consent
Given autopay is enabled for the plan And the reflow increases the next draft amount beyond the configured consent threshold (absolute and/or percentage) When the reflow is applied Then the impacted draft(s) are paused and a consent request is sent (subject to throttle) And no increased draft is executed until explicit member consent is recorded And upon consent, the system records timestamp, actor, method, consent text/version, and threshold basis and reinstates the draft for the new amount/date And if consent is not received by the configured cutoff before draft time, the draft does not run and the member is notified
Autopay Unchanged or Decreased Amounts Proceed
Given autopay is enabled for the plan And the reflow leaves the next draft amount unchanged or decreases it When the reflow is applied Then the draft date and amount are updated to the new schedule without requesting additional consent And only one draft is scheduled per installment (no duplicates) And the consolidated update summarizes the autopay changes
End-to-End Audit Logging and Traceability
Given a reflow event occurs When reminders are rescheduled, superseded reminders canceled, notifications sent, autopay adjusted, or consent requested/recorded Then immutable audit records are written with a shared correlation ID capturing old vs new schedule, reminder IDs created/canceled, notification IDs with template/locale/timestamps, autopay changes (old/new dates/amounts), throttle/deferral decisions, and consent details And audit records are queryable within 60 seconds and retained per retention policy And an administrator can export a chronological reconstruction of the event sequence using the correlation ID

Self-Serve Offers

Members propose a plan themselves with simple sliders for amount, due day, and start date. If the offer fits policy, it auto‑approves; if not, it routes to admins with a prefilled rationale and optional hardship notes. Empowers residents, cuts negotiation time, and gets plans activated faster.

Requirements

Offer Builder Sliders
"As a community member, I want to propose my own payment plan with simple controls so that I can set terms that fit my budget without back-and-forth."
Description

Provide a mobile-first interface that lets members configure a payment plan using simple sliders/inputs for installment amount, due day, and start date. Enforce guardrails (min/max values, step sizes, date bounds) and show real-time validation with inline guidance. Display a dynamic schedule preview (installment count, amounts, total to be repaid, next due date) and highlight any conflicts (overlapping dues, holidays, weekends). Support draft saving, edit/resume, and accessibility (keyboard, screen readers). Ensure the builder consumes policy constraints from the backend and gracefully handles latency and errors.

Acceptance Criteria
Amount Slider Guardrails
Given backend policy provides amount_min, amount_max, and amount_step When the Offer Builder loads Then the installment amount control enforces these bounds and increments Given a user enters an out-of-bounds or non-step amount via keyboard input When the field validates on input or blur Then the value snaps to the nearest valid step within bounds and an inline message states the allowed range and step And the Continue/Submit action remains disabled until the value is valid Given a valid amount is set Then the value is formatted as currency per locale and persisted in the draft Given constraints have not finished loading When the screen first renders Then the amount control is disabled and shows a loading state until constraints are applied
Date Selection Rules (Start Date and Due Day)
Given policy supplies start_date_min, start_date_max, due_day_min, and due_day_max When the user opens date controls Then disabled dates and due day slider range reflect policy bounds Given the user attempts to select a start date outside [start_date_min, start_date_max] Then the selection is blocked and an inline tooltip explains the allowed window Given the user attempts to set a due day outside [due_day_min, due_day_max] Then the slider cannot exceed the limits and manual entry is corrected with an inline message When start date or due day changes Then the next due date recalculates immediately and is displayed next to the controls
Schedule Preview Accuracy and Conflict Highlighting
Given valid amount, due day, and start date are set When any input changes Then the schedule preview updates within 200 ms to show installment count, per-installment amounts, total to be repaid, and next due date Then the sum of installment amounts equals the target balance, with the final installment adjusted by ≤ $0.01 to eliminate rounding drift Given a computed installment falls on a weekend or observed holiday Then the preview marks it and applies the policy-defined shift rule (previous or next business day) and displays the adjustment Given an installment overlaps an existing community due date provided by the backend Then the preview flags the overlap and, per policy, either auto-shifts or blocks submission with a clear inline message
Draft Save, Edit, and Resume
Given an authenticated member is editing a plan When they make any change Then the draft auto-saves within 1 s and shows a "Draft saved" timestamped confirmation Given network failure during auto-save Then the UI shows a non-blocking "Not saved" state with retry And local changes are retained for at least 24 hours Given the member returns later on the same account When opening the Offer Builder Then their latest draft auto-loads and can be edited or discarded Given a successful submission Then the associated draft is marked complete and is not auto-loaded on subsequent visits
Accessibility: Keyboard and Screen Reader Support
Given keyboard navigation When focusing the amount or due day sliders Then Arrow keys adjust by 1 step, PageUp/Down by 5 steps, and Home/End move to min/max; focus order is logical and visible Then all controls expose accessible names and ARIA roles; sliders announce min, max, and current value; inline errors and preview changes announce via polite live regions Then color contrast meets WCAG 2.1 AA and interactive targets are ≥ 44x44 px without relying solely on color to convey meaning
Policy Constraints Fetch, Latency, and Error Handling
Given policy constraints are fetched from the backend When the request takes > 300 ms Then a loading skeleton appears and all interactive controls remain disabled until constraints are applied Given the constraints fetch fails (4xx/5xx) or times out Then an inline error with Retry is shown And submission remains disabled until valid constraints are loaded Given a partial or malformed constraints payload Then safe defaults do not expand limits beyond server-provided values, anomalies are logged, and submission is blocked if required fields are missing Given connectivity is lost mid-session Then the UI indicates offline mode, allows continued editing, and queues sync/retry upon reconnection
Mobile-first Responsiveness and Touch Usability
Given a mobile viewport between 320 and 768 px width Then the builder renders in a single-column layout without horizontal scrolling and respects device safe areas Given touch interaction with sliders When dragging Then values track the finger smoothly (target 60 fps on reference devices) with visual ticks on step changes Given device rotation Then all inputs and scroll position are preserved and the layout reflows without data loss Given thumb reach considerations Then primary actions and sliders are reachable within standard thumb zones; interactive targets are ≥ 44x44 px with at least 16 px spacing
Policy Compliance Engine
"As a board admin, I want offers auto-validated against our policy so that fair, consistent plans are approved instantly without manual review."
Description

Implement a configurable rules engine that evaluates proposed offers against community policy in real time and at submission. Support rule types such as minimum payment percentage, maximum term length, fee caps, grace periods, cutoff dates, delinquency state requirements, and plan concurrency limits. Return a deterministic pass/fail decision with human-readable reasons and suggested adjustments. Enable rule versioning, per-community configurations, and auditability of evaluations. Expose synchronous APIs for the UI and asynchronous checks for back-office flows. Ensure performance at scale and secure, least-privilege access to policy data.

Acceptance Criteria
UI Synchronous Evaluation — Auto-Approval Path
Given a member proposes an offer that satisfies the active community policy (minimum payment percentage, maximum term length, fee caps, allowed due day, start date within grace and before cutoff) When the UI calls the synchronous evaluate API with the offer and member context (communityId, delinquency state, existing plans) Then the API responds within p95 <= 150 ms and p99 <= 300 ms And decision = "pass" And reasons = [] And suggestions = [] And the returned normalized schedule totals equal the offered amount within $0.01 rounding And evaluationId and policy versionId are included in the response And an immutable audit record is persisted containing evaluationId, communityId, versionId, inputs hash, and rule outcomes
UI Synchronous Evaluation — Violations with Suggestions
Given an offer violates minimum payment percentage and maximum term length rules When the synchronous evaluate API is invoked with the offer Then decision = "fail" And reasons includes codes ["MIN_PAYMENT_PERCENT","MAX_TERM_LENGTH"] with human-readable messages And suggestions include an adjusted amount and an adjusted term that jointly satisfy policy And applying the suggestions to the same input yields decision = "pass" And the order of reasons and suggestions is deterministic by rule priority And the API returns HTTP 200 with the decision payload even when decision = "fail" (no transport error on policy failure)
Asynchronous Back-Office Check — Consistency and SLA
Given a plan enters pending-review or a delinquency-state change event occurs When the event triggers an asynchronous policy evaluation Then the job is enqueued with a correlationId and processed within p95 <= 2 minutes and p99 <= 5 minutes And for identical inputs and versionId, the async decision equals the synchronous decision And the result is published to the back-office topic/queue with correlationId and evaluationId And the evaluation is idempotent (duplicate messages are detected and not double-processed) And failures retry with exponential backoff up to 3 attempts and surface to operations after exhaustion And an audit record is persisted with actor = "system" and the same versionId
Per-Community Configuration and Versioning with Effective Dating
Given multiple rule versions exist for a community with distinct effective_at timestamps When evaluating at timestamp T with that communityId Then the engine selects the latest version whose effective_at <= T And the response includes versionId and configHash And draft or inactive versions are not selectable by the evaluator And admins can request evaluation "asOf" a specific timestamp T to test a future version And changing the active version does not alter stored results of past evaluations And evaluating for a non-existent community or version returns 404 with no policy details
Rule Coverage, Precedence, and Determinism
Given the rules engine is configured Then it supports and evaluates: minimum payment percentage, maximum term length, fee caps, grace periods, cutoff dates, delinquency state requirements, and plan concurrency limits And each rule has unit tests for pass and fail cases verifying decision, reason code, and message And precedence is enforced: hard-stop eligibility rules (delinquency state, plan concurrency, cutoff date) evaluate before quantitative rules And hard-stop failures suppress suggestion output while still returning reasons And evaluation is deterministic: same inputs and versionId always produce the same decision, ordered reasons, and suggestions And monetary calculations use two-decimal precision with banker's rounding where applicable
Security and Least-Privilege Access Controls
Given API tokens with scopes evaluate:write (evaluate), policy:read (read config), and policy:admin (manage config) When a caller lacks the required scope or does not belong to the target community Then the request is rejected with 403 and no policy details And a caller with evaluate:write for community X can read only policy for community X during evaluation And cross-community data access attempts are denied and logged And PII fields are redacted in logs and audit records And all endpoints require TLS 1.2+ and return HSTS And access events capture actorId, scopes, communityId, and evaluationId for auditing
Performance and Scale Benchmarks
Given a load of 500 synchronous evaluations per second across 200 communities and up to 50 active rules per community When the engine evaluates offers under sustained load Then p95 latency <= 150 ms and p99 <= 300 ms And 5xx error rate < 0.1% and timeouts < 0.5% And CPU utilization < 70% and memory < 75% of container limits at steady state And the system scales horizontally with near-linear throughput (within ±10%) when replicas double And async workers process >= 10,000 evaluations per minute with p95 completion <= 2 minutes And the service degrades gracefully under overload using rate limiting and circuit breakers (no partial approvals returned)
Eligibility & Balance Sync
"As a member, I want my offer to reflect my true current balance and commitments so that I don’t over- or under-commit."
Description

Integrate with the ledger to compute eligible balances in real time, considering outstanding charges, credits, disputed items, existing plans, and special assessments. Block duplicate or overlapping plans, enforce one-plan-per-account rules (configurable), and reflect partial payments made during the offer flow. Handle proration for mid-cycle starts and dynamically calculate minimum viable installment amounts based on remaining balance and term limits. Provide resilient handling of stale data, concurrency conflicts, and offline adjustments with idempotent submissions.

Acceptance Criteria
Real-Time Eligible Balance Calculation
Given an account with outstanding charges, credits, disputed items, existing payment plans, and special assessments When the Self‑Serve Offer builder loads or refreshes ledger data Then Eligible Balance = (sum of unpaid charges + special assessments) − credits − amounts already scheduled under active/pending plans, excluding disputed items from the sum And the Eligible Balance is computed within 1 second of data retrieval and displays a last-updated timestamp And if the ledger service is unreachable, the Eligible Balance section shows an error state and Submit is disabled until a successful recalculation occurs
Duplicate and Overlapping Plan Prevention
Given an account has an active or pending plan When a user attempts to submit a new plan that overlaps in covered balance or schedule Then submission is blocked with a clear, actionable message and no plan is created Given the org setting onePlanPerAccount = true When any second plan submission is attempted Then submission is blocked regardless of overlap and no plan is created Given the org setting onePlanPerAccount = false When a second plan covers non-overlapping ledger items and does not double-allocate any charge Then submission is allowed Given a race condition where another plan is created between review and submit When the user submits Then the system detects the conflict and blocks with a conflict message; no duplicate plan is created
In-Flow Partial Payments Reconciliation
Given the user has the offer builder open When a partial payment or backdated adjustment posts to the ledger for items included in the Eligible Balance Then the builder recalculates Eligible Balance and the minimum installment within 1 second and displays a non-blocking banner describing the change And the user’s current slider selections are preserved if still valid; otherwise, invalid inputs are highlighted and Submit is disabled until resolved And if the recalculation reduces the remaining balance to zero, plan creation is disabled and the user is prompted to close the builder
Mid‑Cycle Start Proration
Given the user selects a plan start date that falls mid-billing-cycle When calculating the first installment Then the amount is prorated by days remaining in the cycle per policy formula, and the schedule displays the prorated first installment with an explanation note And if proration computes below the policy minimum installment, the first installment is merged/adjusted per policy so no installment is below the minimum And when the user changes the start date, the proration and schedule recalculate within 1 second
Minimum Viable Installment Enforcement
Given remaining eligible balance R and a maximum term limit T When the user adjusts amount/term sliders Then the system enforces a minimum installment >= ceil(R / T) and prevents submission if the selected installment is below this threshold or violates policy minimums And if policy defines an absolute minimum installment M, when R/T < M then M is enforced as the minimum And if policy defines a maximum plan duration D, when the user’s selection would exceed D then the term is capped at D and the minimum installment recalculates accordingly And when R changes due to payments or adjustments, the enforced minimum updates within 1 second
Stale Data and Concurrency Conflict Handling
Given the offer builder loaded with ledger version V When any relevant ledger change occurs before submission Then submission sends version V and fails with 409 Conflict if the current version != V, with a prompt to refresh; no plan is created And when the user refreshes, all computed values (eligible balance, proration, minimums) are recalculated and the form is revalidated before enabling Submit And on transient API failure during refresh, up to 3 retries with exponential backoff are attempted, and the UI offers a manual retry without creating any partial resources
Idempotent Submission on Retries
Given the client attaches an idempotency key to the plan submission When the network times out and the client retries within 24 hours Then exactly one plan is created and subsequent responses return the same plan ID and schedule Given the initial attempt succeeded server-side but the client did not receive a response When the client retries with the same idempotency key Then the server returns a success with the original resource and no duplicate ledger mutations occur Given a duplicate submission without an idempotency key that matches an identical payload within the last 60 seconds When detected Then the server rejects with 409 Conflict to prevent duplicate plan creation
Auto-Approval & Admin Routing
"As an admin, I want non-compliant offers automatically routed with clear context so that I can quickly decide or suggest changes."
Description

Automatically approve offers that satisfy policy and immediately confirm terms to the member. For non-compliant offers, create an item in an admin review queue with prefilled rationale (failed rules, suggested changes, and risk indicators). Support overrides with reason capture, SLA timers, assignment, bulk actions, and safe rollbacks. Provide structured request/response objects so the UI can present next steps (revise terms, add documents, or await decision). Ensure consistent state transitions and idempotent processing to prevent duplicate approvals.

Acceptance Criteria
Auto-Approve Policy-Compliant Offer
Given a member submits an offer whose amount, due day, start date, and term satisfy all active policy rules When the offer is submitted Then the system auto-approves within 2 seconds And generates a payment schedule matching the submitted terms And persists the decision with decision_reason = policy_compliant_auto_approval and the evaluated rule results And returns a response containing state = approved, planId, schedule, nextSteps = [confirmSetup] And sends a confirmation message to the member
Admin Queue Routing for Non-Compliant Offer
Given a member submits an offer that violates one or more policy rules When the offer is submitted Then the system creates a single admin review queue item with status = awaiting_review And the queue item includes failedRules[], suggestedChanges, riskIndicators[], submittedTerms, hardshipNotes (if provided) And assigns the item using routing rules (owner or team) and sets createdAt and dueAt (per SLA policy) And returns a response containing state = queued, queueItemId, nextSteps including one of [reviseTerms, awaitDecision] And no plan is activated
Override Decisions with Reason Capture and Audit
Given an authorized admin opens a queued offer When the admin approves or declines with override Then they must select a reasonCode from the configured list and enter a free-text note of at least 10 characters And the system records actor, timestamp, previous_state, new_state, reasonCode, note, and any attachments in an immutable audit log And the offer transitions to admin_approved or declined accordingly And member and assignees are notified of the decision And the response includes state, nextSteps, and override metadata
SLA Timers, Assignment, and Escalation
Given a queue item is created When severity is high, medium, or low per rule outcome Then dueAt is set to now+4h (high), next_business_day (medium), or 3_business_days (low) And the item may be assigned to a user or team and reassigned without losing audit history When the current time passes dueAt and the item is not resolved Then the system marks sla_breached = true and sends an escalation notification to the escalation target And time_to_first_touch and time_to_resolution are captured for reporting
Bulk Actions with Safeguards and Safe Rollbacks
Given an admin selects 2 to 100 queued items for a bulk action (approve, decline, request-info) When the action is confirmed Then the system validates each item is eligible for the chosen action and skips ineligible ones with reasons And processes eligible items atomically per item and returns a summary containing counts (processed, skipped) and per-item outcomes And for bulk approvals the system verifies no conflicting active plan or posted payment exists before activation And the system allows rollback of a bulk action per item within 30 minutes provided no payment has been posted and state has not changed since And all actions and rollbacks are audit logged
Idempotent Processing and Consistent State Transitions
Given duplicate submissions with the same idempotencyKey within 24 hours When the offer endpoint is called multiple times Then the system returns the original decision response and does not create duplicate plans or queue items And background jobs and webhooks are safe to retry without duplicating side effects And state transitions follow the defined graph: draft -> submitted -> (approved|queued) -> (admin_approved|declined) -> activated, and any invalid transition returns HTTP 409 with code = invalid_state_transition
Structured Request/Response Contract for UI Next Steps
Given a client submits an offer request When residentId, terms (amount, dueDay, startDate), and idempotencyKey are provided and valid Then the API returns HTTP 200 with a response containing decisionState, planId or queueItemId, nextSteps (values only from [confirmSetup, reviseTerms, addDocuments, awaitDecision]), rationale (failedRules and suggestions), and schemaVersion When required fields are missing or invalid Then the API returns HTTP 422 with per-field error details And end-to-end response time is P95 <= 500 ms under expected load
Activation & Billing Integration
"As a member, I want my approved offer to automatically set up payments and reminders so that I can stay on track without manual follow-up."
Description

Upon approval, automatically instantiate a payment schedule, generate corresponding invoices/commitments, and link them to the resident’s ledger and feed. Enable optional autopay enrollment, set up reminder cadence, and configure retries and grace periods per policy. Support plan edits (admin-approved), catch-up logic for missed payments, and cancellation with clear settlement outcomes. Ensure all ledger updates are atomic and auditable, and expose webhooks/events for downstream systems (notifications, exports).

Acceptance Criteria
Auto-Activation Creates Schedule, Invoices, and Links
Given an offer meets policy and is approved (auto or admin) When the plan is activated Then a payment schedule is created with installments that match amount, start date, and due-day cadence defined by the offer and policy Given the created schedule When invoices/commitments are generated Then one invoice/commitment exists per installment with correct due date, amount, and unique identifiers Given invoices are generated When ledger entries are posted Then each invoice is linked to the resident’s ledger account and a feed item is created referencing the invoice and schedule Given activation completes When events are emitted Then an ActivationCreated and InvoiceCreated event is published for each resource with resident ID, plan ID, amounts, due dates, and correlation/idempotency keys
Atomic Ledger Updates and Audit Trail
Given activation starts When any step of schedule/invoice/ledger creation fails Then no partial invoices, ledger entries, or feed items persist and the operation returns an error with a retry-safe idempotency key Given the same activation request is retried with the same idempotency key When the previous attempt succeeded Then no duplicate schedules, invoices, or ledger entries are created Given any change to schedule, invoice, or ledger When the change is committed Then an immutable audit record is stored capturing actor (system/admin), timestamp (UTC), before/after values, rationale/notes, and origin request ID
Autopay Enrollment and Execution
Given a resident activates a plan and opts into autopay with a valid payment method When an installment reaches its due date Then the system attempts payment on the due day at the policy-defined time window and records success in the ledger within 5 minutes Given an autopay attempt fails When retries are configured by policy Then retries occur per policy (count and interval), each attempt logged, and an InvoicePaymentFailed event is emitted per failure Given autopay is enabled When the plan is canceled or fully paid Then autopay is disabled immediately and no further attempts are made
Reminder Cadence and Logging
Given a plan is activated When reminders are configured by policy (e.g., N days before due, on due day, M days after) Then reminder jobs are scheduled accordingly and visible in the plan’s activity log Given a reminder is sent When the delivery occurs Then a ReminderSent event is emitted with channel, timestamp, and target invoice ID, and the reminder is marked delivered with delivery metadata Given a reminder fails to deliver When a retry policy exists Then delivery is retried per policy and failures are logged without duplicating user-visible messages
Admin-Approved Plan Edits Affect Future Periods Only
Given an admin submits an edit to an active plan (amount, due day, start date, term) When the edit is approved Then changes apply to unpaid future installments only; posted invoices and ledger entries remain unchanged Given future installments are recalculated When new invoices are generated Then invoice amounts, counts, and due dates reflect the updated policy, and an EditApplied event summarizes deltas Given an edit is applied When the system records the change Then an audit entry captures approver, rationale, old vs new values, and timestamp
Missed Payment Catch-Up Logic
Given an installment remains unpaid past the grace period When catch-up is required by policy Then a catch-up amount is appended to the next invoice or a standalone catch-up invoice is generated per policy, without duplicating principal or fees Given catch-up is generated When ledger entries are posted Then the ledger clearly itemizes original installment, fees, and catch-up components and links them to source invoices Given catch-up exists When autopay runs on the next cycle Then the total attempted equals scheduled installment plus catch-up amount, within network limits, and outcomes are logged
Cancellation and Settlement Outcomes
Given an admin cancels an active plan When settlement policy is applied Then the system computes outstanding balance and generates either a final invoice due immediately or a credit/write-off per policy, and updates the ledger accordingly Given a plan is canceled When pending jobs exist (reminders, retries, autopay) Then all future jobs are canceled and a PlanCanceled event is emitted with settlement details Given cancellation completes When the resident views the feed Then a feed item displays the cancellation and settlement outcome with clear totals and next steps
Hardship Notes & Attachments
"As a resident, I want to include context and documents with my offer so that admins understand my situation if a review is needed."
Description

Provide an optional hardship section where members can select a reason, enter free-text context, and upload supporting documents (images/PDFs). Store artifacts securely with role-based access controls, retention policies, and redaction tools for sensitive data. Associate notes and files with the offer for admin review, and make fields configurable (required when exceeding certain policy thresholds). Validate file types/size, capture consent for data use, and log access for compliance.

Acceptance Criteria
Hardship Notes Entry & Consent Capture
Given a member is proposing a Self‑Serve offer and opens the Hardship section When the form loads Then a configurable list of hardship reasons is displayed Given the default configuration When no policy thresholds are exceeded Then hardship reason and context fields are optional Given the member selects a hardship reason When "Other" is selected Then a free‑text context field becomes required and supports up to 2000 characters with a live character counter Given the free‑text context field When input exceeds 2000 characters Then extra characters are blocked and an inline validation error is shown Given the hardship section is present When the member attempts to submit the offer Then a consent checkbox with prescribed text is displayed, must be checked to proceed, and the system stores consent with timestamp, user id, and consent text version
Policy‑Driven Required Fields
Given an admin‑configured policy threshold (e.g., amount > X or term > Y months) When a member’s proposed plan exceeds the threshold Then hardship reason and free‑text context fields become required before submission Given the policy configuration screen When an admin sets thresholds and selects which fields become required Then these settings persist and take effect for new offers within 1 minute of save Given a member submits under threshold‑triggered conditions When required hardship fields are missing Then submission is blocked and specific inline errors are shown next to each missing field
Attachments Upload Validation
Given the hardship section When uploading files Then only PDF, JPG, JPEG, and PNG are accepted; other types are rejected with a clear error message Given a valid file type When the file size exceeds 10 MB per file Then the upload is rejected with a size‑specific error Given multiple files are uploaded When total size exceeds 50 MB or file count exceeds 10 Then the upload is rejected with a clear error Given an upload fails validation When the member corrects the issue Then the member can reattempt upload without refreshing the page Given a successful upload When the offer is submitted Then attachments are persisted and linked to the offer
Offer Association & Admin Review Visibility
Given a member submits an offer with hardship notes and attachments When the offer is created Then the notes, consent record, and attachments are linked to the offer ID and versioned with the submission Given an admin opens the offer review page When the offer contains hardship artifacts Then the admin can view reason, context, consent record, and attachment list in a single panel Given the admin downloads an attachment When the file is served Then the download is logged with user id, role, timestamp, and file id Given the offer is updated by the member prior to admin decision When hardship notes or attachments change Then previous versions remain accessible to admins with appropriate permissions
RBAC, Secure Storage, and Access Logging
Given the offer owner When viewing their own offer Then they can view and download their own hardship notes and attachments Given any non‑owner member When attempting to access another member’s hardship artifacts Then access is denied with HTTP 403 and no artifact metadata is leaked Given an authorized admin role When accessing hardship artifacts Then access is permitted and all actions (view, download, redact, delete) are logged with user id, role, action, resource id, timestamp, and IP address Given stored hardship artifacts When at rest or in transit Then the data is encrypted at rest and transmitted over TLS Given access logs When viewed by an admin with audit permissions Then logs are filterable by offer id, user id, action, and date range
Data Retention Policies and Deletion
Given an admin configures a retention policy for hardship artifacts When saved Then the retention period is stored and applied to new and existing artifacts Given artifacts reach end of retention and are not on legal hold When the nightly retention job runs Then artifacts and their access logs are purged, and a purge record is created with timestamp and count Given a member withdraws an offer or requests deletion When permitted by policy Then artifacts are soft‑deleted within 24 hours and remain eligible for purge per retention rules Given an artifact is on legal hold When retention is evaluated Then the artifact is not purged until the hold is removed
Redaction Tools for Sensitive Data
Given an admin is viewing an attachment When selecting Redact Then the admin can apply redaction boxes to images and area‑based redaction to PDFs before saving Given a redaction is saved When the system generates the redacted version Then non‑privileged users see only the redacted derivative while the original is restricted to admins with "View Original" permission Given a redacted file is served When inspected Then redacted regions are permanently removed from the derivative (not an overlay) Given a redaction is performed When saved Then a redaction event is logged with user id, optional reason, timestamp, file id, and version
Notifications & Activity Feed Updates
"As a member, I want timely updates about my offer’s status so that I know what to expect and what to do next."
Description

Send real-time confirmations and status updates for offers via the Duesly feed, email, and SMS (where enabled). Include deep links back to the offer, clear next steps, and due dates. Notify admins when a review is required and when SLAs are at risk. Provide localized, brandable templates with quiet hours, batching, and opt-out controls that respect regulatory requirements. Ensure all notifications and feed posts are deduplicated, traceable, and aligned with the plan’s lifecycle events.

Acceptance Criteria
Member Receives Real-Time Offer Confirmation
Given a member submits a self-serve offer with in-app feed and email enabled, When the offer is auto-approved, Then a feed post and email are created within 10 seconds containing the offer ID, deep link to the offer, clear next steps, and first due date. Given SMS is enabled for the member, When the auto-approval occurs, Then an SMS is sent within 30 seconds containing a concise confirmation and deep link. Given a channel is disabled for the member, When the auto-approval occurs, Then no message is sent to that channel and no error is logged. Given messages are sent, When delivery completes, Then the activity feed shows one deduplicated confirmation post with a per-channel delivery status summary.
Admin Notified for Out-of-Policy Offer Review
Given a member submits an offer that fails policy checks, When the offer is created, Then an admin review task is generated and a feed post is created tagging the assigned admin group with a deep link to the review page. Given admin notification preferences include email and/or SMS, When the review task is generated, Then notifications are sent per preference within 60 seconds including the prefilled rationale and any hardship notes. Given SLAs define a review window (e.g., 48 hours), When the task is created, Then the feed post and notifications display the SLA due date/time in the community’s timezone. Given quiet hours are active, When the task is created during quiet hours, Then out-of-app notifications are queued for release at quiet hours end while the in-app feed post is created immediately.
SLA Risk and Breach Escalations to Admins
Given a review task is pending, When 80% of the SLA window has elapsed without decision, Then an "SLA at risk" notification is generated, respecting quiet hours (queued if within quiet hours) and delivered within 5 minutes otherwise. Given the SLA due time passes without decision, When breach occurs, Then an "SLA breached" feed banner is posted immediately and out-of-app notifications are sent within 5 minutes if outside quiet hours or queued until quiet hours end if inside. Given escalations are generated, When delivery completes or fails, Then channel outcomes are recorded in the audit log with timestamps and error codes where applicable.
Localized and Brandable Templates
Given the community has a primary locale and branding configuration, When a notification is generated, Then the template renders in that locale (language and date/time formats) and applies logo/colors per brand configuration. Given a member’s preferred locale differs from the community default, When rendering a notification to that member, Then the member’s locale is used. Given a translation key is missing, When rendering occurs, Then the system falls back to the default locale without exposing placeholder keys and logs a warning. Given required template variables (offer_id, amount, due_date, deep_link, next_steps) are present, When rendering occurs, Then all variables resolve to non-empty values; otherwise the message is not sent and a blocking error is logged.
Quiet Hours, Batching, and Opt-Out Compliance
Given community quiet hours are configured (e.g., 21:00–08:00 local), When a non-urgent notification is generated during quiet hours, Then email/SMS are queued for delivery immediately after quiet hours and the in-app feed post is created immediately. Given multiple non-urgent notifications for the same recipient are queued within a 15-minute window, When they are released, Then they are batched into a single email/SMS summarizing up to 10 items with individual deep links, respecting provider rate limits. Given a member replies STOP to SMS or toggles off SMS in preferences, When subsequent notifications are queued, Then SMS is suppressed and a suppression reason is recorded; in-app and email (if enabled) still deliver. Given applicable regulations require email unsubscribe links, When emails are sent, Then they include a functioning manage-preferences/unsubscribe link that updates preferences within 60 seconds of click.
Deduplication and Traceability of Notifications
Given an identical lifecycle event is processed more than once (e.g., retries), When notifications are generated, Then idempotency keys prevent duplicate feed posts and prevent duplicate sends per channel for the same event_id and recipient. Given any notification is sent, When auditing later, Then an immutable log record exists with event_id, offer_id, member_id, message_id, channel, locale, created_at, sent_at, status (queued|sent|delivered|failed|suppressed), and actor/system. Given an email/SMS provider retries delivery, When duplicates would be received, Then only one message is rendered to the recipient and duplicate attempts are logged as suppressed-duplicate. Given an administrator searches the audit log by offer_id or member_id and date range, When executed, Then matching records return within 2 seconds for up to 1,000 records with pagination for larger result sets.
Lifecycle Event Coverage and Content Accuracy
Given key lifecycle events (submitted, auto_approved, needs_review, admin_approved, declined, activated, payment_due, payment_received, delinquent, canceled, modified) are defined, When any event occurs, Then a mapped notification policy specifies recipients, channels, and templates for that event. Given an event notification is sent, When content renders, Then the message includes a deep link to the relevant offer/plan or payment schedule, a clear next step for the recipient, and the applicable due date/time. Given the payment_received event occurs, When the notification is delivered, Then the displayed amount, receipt ID, and remaining balance match the ledger within $0.01 and the feed reflects the updated balance. Given an event lacks a valid template or mapping, When the event fires, Then no notification is sent, a high-severity error is logged with the event_id, and the admin is alerted via the dashboard within 1 minute.

Fairness Guardrails

Define eligibility and consistency rules—minimum first payment, fee‑waiver conditions while current, max plans per year, and cooldowns after default. Duesly enforces them automatically and records exceptions with reasons. Ensures equal treatment across households and makes decisions audit‑ready.

Requirements

Eligibility Rule Engine
"As a board treasurer, I want to define and consistently apply eligibility rules for plans and waivers so that every household is treated equally and we reduce subjective, ad‑hoc decisions."
Description

Implements a configurable policy engine to define and evaluate fairness guardrails across households. Supports key rule types such as minimum first payment (fixed amount or percentage), fee‑waiver conditions only while an account is current (no past‑due balance), maximum number of payment plans per rolling period (e.g., per 12 months), and cooldowns after default (e.g., X days from last plan default before eligibility resumes). Rules reference standardized data points (current balance, days past due, last default date, number of plans started/completed/failed, household status) and can be composed with AND/OR operators. Evaluation is deterministic and time‑zone aware (using the community’s configured time zone). Exposes a service and API that return allow/deny with machine‑readable codes and human‑readable rationale for UI display and audit. Ensures equal treatment by applying the same logic to all households and producing consistent outcomes across the platform.

Acceptance Criteria
Minimum First Payment Rule Enforcement
Given a configured rule min_first_payment = 20% of outstanding balance and outstanding_balance = $500.00, When initial_payment = $99.99 (rounded half-up to cents), Then decision = "deny" with code = "MIN_FIRST_PAYMENT_NOT_MET" and rationale includes required_minimum = $100.00. Given the same rule and initial_payment = $100.00, When evaluated, Then decision = "allow" with code = "OK" and rationale states minimum met at $100.00. Given a configured rule min_first_payment_fixed = $150.00, When initial_payment = $149.99, Then decision = "deny" with code = "MIN_FIRST_PAYMENT_NOT_MET"; When initial_payment >= $150.00, Then decision = "allow". Given both percentage and fixed minimum are configured, When evaluated, Then the engine enforces the higher computed minimum amount.
Fee-Waiver Eligibility While Current
Given current_balance = 0 and days_past_due = 0, When fee-waiver eligibility is evaluated, Then decision = "allow" with code = "ACCOUNT_CURRENT" and rationale indicates no past-due balance. Given current_balance > 0 or days_past_due > 0, When evaluated, Then decision = "deny" with code = "ACCOUNT_NOT_CURRENT" and rationale states fee waiver applies only while account is current. Given the account settles the past-due balance to 0 in the community time zone, When re-evaluated, Then decision = "allow" with code = "ACCOUNT_CURRENT".
Max Payment Plans Per Rolling Period
Given rule max_plans_per_rolling_period = 2 over period_length = 12 months and metric = "started", When the household has 2 plans with started_at timestamps within the last 12 months in the community time zone, Then decision = "deny" with code = "MAX_PLANS_REACHED" and rationale lists the counted plans. Given one prior plan started 12 months and 1 minute before now (in community TZ), When evaluated, Then that plan is excluded from the rolling window and decision = "allow" if the count < 2. Given metric is set to "completed" or "failed", When evaluated, Then the engine counts plans by completed_at or failed_at respectively to determine the limit.
Cooldown After Default
Given rule cooldown_after_default_days = 90 and last_default_at = 2025-05-01T10:00 in the community time zone, When evaluated at 2025-07-30T09:59 in the same time zone, Then decision = "deny" with code = "COOLDOWN_ACTIVE" and rationale includes remaining time. Given the same inputs, When evaluated at 2025-07-30T10:00 or later in the community time zone, Then decision = "allow" and code = "OK" (cooldown satisfied). Given no recorded last_default_at, When evaluated, Then the cooldown rule does not block eligibility and contributes no deny code.
Rule Composition with AND/OR Operators
Given rule A = "min first payment met" and rule B = "account current" and composition = A AND B, When A = true and B = false, Then overall decision = "deny" with codes including "ACCOUNT_NOT_CURRENT" and rationale enumerates A = true, B = false. Given composition = A OR B, When A = false and B = true, Then overall decision = "allow" with code = "OK". Given grouped composition = (A AND B) OR C with C = true, When A = false and B = false, Then overall decision = "allow" due to C and rationale reflects grouping. Given identical inputs and composition, When evaluated repeatedly, Then the set and order of rule outcomes in the audit trail are consistent across runs.
Deterministic, Community Time Zone Aware Evaluation
Given identical inputs including the same evaluation_instant and community_tz, When the engine is executed multiple times, Then the decision, codes, and rationale are identical across executions. Given community_tz = "America/Chicago" and later community_tz = "Europe/Berlin" with the same absolute instant, When evaluating rolling windows and cooldowns, Then boundaries are computed using the configured community_tz and outcomes reflect that time zone. Given a decision response, When inspected, Then it includes evaluation_timestamp in ISO 8601 with offset and the community_tz IANA identifier used for computation.
API Decision Response and Audit Logging
Given an evaluation request via the eligibility API, When the engine returns a result, Then the response JSON includes fields: decision in {"allow","deny"}, codes (array of machine-readable codes), rationale (<= 500 characters), evaluation_timestamp (ISO 8601), community_tz (IANA), and input_snapshot (standardized data points used). Given a deny decision is manually overridden by an authorized user providing a required reason, When the override is saved, Then an immutable audit record is created linking to the original evaluation id with fields: prior_decision = "deny", post_decision = "allow", reason (non-empty), actor_id, actor_role, and timestamp. Given audit queries by household_id and date range, When requested, Then audit entries are returned in chronological order and include the machine-readable codes from the original evaluation. Given versioned machine-readable codes, When the service is upgraded, Then previously stored codes remain unchanged and still resolve in documentation.
Admin Rule Builder UI
"As a part‑time manager, I want an easy UI to configure and test guardrails without code so that I can adjust policy quickly and confidently."
Description

Provides a non‑technical interface for creating, editing, testing, and publishing fairness rules. Includes guided presets for common HOA policies (e.g., minimum 25% first payment, max 2 plans/year, 60‑day cooldown after default) and inline definitions for key terms like “current.” Offers validation, preview, and a test sandbox using sample or real households to show pass/fail and rationale before publishing. Supports staged changes with effective dates and optional draft review, plus clear warnings when a rule change would tighten or loosen eligibility. Integrates directly with the rule engine and uses the same API contracts to ensure what’s configured is exactly what is enforced.

Acceptance Criteria
Create Rule from Preset with Inline Definitions
Given an admin opens the Rule Builder And selects the preset "Minimum first payment — 25%" When the preset loads Then the "Minimum first payment" field value equals 25% And the inline definition for the term "current" opens on click/hover and displays the glossary text for key "current" And editing any preset field enables "Save draft"
Rule Input Validation Blocks Publish
Given an admin enters invalid values (e.g., Minimum first payment < 0% or > 100%, Max plans/year < 1, Cooldown days < 0) When the admin clicks Publish Then field-level errors display with messages: - "Minimum first payment must be between 0% and 100%" - "Max plans per year must be 1 or greater" - "Cooldown must be 0 or greater days" And the Publish action is disabled until all errors are resolved And server-side validation returns matching errors if bypass is attempted
Eligibility Impact Analysis and Warning Banner
Given there is an active rule version V1 and a draft version V2 with changes When the admin clicks Preview impact Then the UI displays counts "Lose eligibility: X" and "Gain eligibility: Y" based on the current household dataset snapshot And a banner states "This change tightens eligibility" when X > Y, or "This change loosens eligibility" when Y > X And a CSV of affected households is downloadable
Test Sandbox Shows Pass/Fail and Rationale
Given the admin selects sample and/or real households in the sandbox When the admin clicks Run test Then each household row shows Pass/Fail for eligibility and a rationale trace listing evaluated rules, inputs, and outcomes And sandbox results match the rule engine dry-run API for the same inputs And results are exportable to CSV
Schedule Future-Dated Rule and Stage Changes
Given the admin sets an effective date/time in the future When the admin publishes the draft Then the new version status is "Scheduled" with the selected timestamp And the current version remains "Active" until the effective timestamp, then switches automatically without downtime And the admin can cancel the scheduled change prior to the effective timestamp
Draft Review and Approval Workflow
Given review workflow is enabled for the community When an editor submits a draft for review Then the draft status becomes "Pending Review" and an approver is notified And the approver can Approve with comment to enable Publish or Request changes with comment to return to Draft And when review workflow is disabled, an admin with publish permission can publish directly
API Parity and Contract Enforcement
Given the UI generates a rule set JSON payload for the draft When the admin publishes the draft Then the payload validates against the rule engine JSON schema; mismatches are rejected with error details And the published version ID and checksum are stored and sent to the rule engine And evaluating identical test inputs via the UI sandbox and the rule engine evaluate endpoint yields the same Pass/Fail and rationale outputs
Automated Enforcement Hooks
"As a payment admin, I want automatic rule checks at the point of action so that noncompliant plans and waivers are prevented and residents receive clear guidance on what to do next."
Description

Integrates guardrail checks into critical workflows and APIs: creating payment plans from a bill post, granting fee waivers, approving partial/first payments, re‑enrolling after a default, and reversing defaults. Blocks noncompliant actions and displays clear, contextual explanations (e.g., “Cooldown in effect until Oct 15, 2025” or “Minimum first payment is 25%”). Enforcement occurs at both UI and API layers to prevent circumvention. Includes background jobs to update eligibility as cooldowns expire and to notify users when they become eligible again. Ensures that any action initiated from Duesly’s one‑click bill or compliance feed is evaluated consistently before execution.

Acceptance Criteria
Create Payment Plan from Bill Post — Enforce Minimum First Payment and Plan Limits
Given org guardrails set min_first_payment_percent = 25 and max_plans_per_year = 1 and household H has created 1 plan in the last 365 days When H attempts to create a new payment plan from a bill post with first_payment_amount that is >= 25% of outstanding Then the action is blocked and the UI shows an inline banner: "Max payment plans per year reached. Next eligible on <MMM DD, YYYY>" and the API responds 409 with code = GUARDRAIL_MAX_PLANS_PER_YEAR and metadata.nextEligibleDate populated, and no plan is created Given org guardrails set min_first_payment_percent = 25 and household H is within plan limit When H attempts to create a new payment plan from a bill post for $400 with first_payment_amount = $80 (20%) Then the action is blocked and the UI shows an inline banner: "Minimum first payment is 25% ($100.00)" and the API responds 409 with code = GUARDRAIL_MIN_FIRST_PAYMENT and metadata.minPercent = 25 and metadata.minAmount = 100.00, and no plan is created Given org guardrails set min_first_payment_percent = 25 and household H is within plan limit When H attempts to create a new payment plan from a bill post with first_payment_amount >= the required minimum Then the plan is created, the UI shows a success confirmation, and the API returns 201 with planId, and an audit event guardrail.check = "passed" is recorded
Fee Waiver Request — Enforce 'While Current' Rule
Given org guardrail waiver_requires_current = true and household H has any overdue balance or unpaid violation fine When an authorized manager attempts to grant a fee waiver from the bill/compliance feed Then the action is blocked and the UI shows: "Fee waivers are available only while the account is current" and the API responds 409 with code = GUARDRAIL_WAIVER_REQUIRES_CURRENT, and no waiver is applied Given org guardrail waiver_requires_current = true and household H is current (no overdue balances or unpaid fines) When an authorized manager grants a fee waiver from the bill/compliance feed Then the waiver is applied, the UI shows a success confirmation, the API returns 201 with waiverId, and an audit event waiver.granted with guardrail.check = "passed" is recorded
Partial/First Payment — Enforce Minimum First Payment Percentage
Given org guardrails set min_first_payment_percent = 25 and a payer attempts a first or partial payment below the required threshold for the associated bill/plan When the payment is submitted via UI or API Then the action is blocked and the UI shows: "Minimum first payment is 25% ($<computedAmount>)" and the API responds 409 with code = GUARDRAIL_MIN_FIRST_PAYMENT and metadata.minPercent and metadata.minAmount populated, and no payment is recorded Given org guardrails set min_first_payment_percent = 25 and a payer attempts a first or partial payment at or above the required threshold When the payment is submitted via UI or API Then the payment is accepted and recorded, the plan (if any) transitions to Active if this is the first payment, and the API returns 201 with paymentId
Re-enrollment After Default — Enforce Cooldown and Plan Limits
Given household H defaulted on a payment plan at date D and org guardrail cooldown_days = 60 When H attempts to create a new payment plan before D + 60 days Then the action is blocked and the UI shows: "Cooldown in effect until <MMM DD, YYYY>" and the API responds 409 with code = GUARDRAIL_COOLDOWN_ACTIVE and metadata.cooldownEndDate populated, and no plan is created Given household H defaulted on a payment plan at date D and org guardrails cooldown_days = 60 and max_plans_per_year = 1 and H has already created 1 plan in the last 365 days When H attempts to create a new plan on or after D + 60 days Then the action is blocked and the UI/API return the max plans per year violation as the definitive reason (code = GUARDRAIL_MAX_PLANS_PER_YEAR) Given household H defaulted on a payment plan at date D and org guardrail cooldown_days = 60 and H is within plan limits When H attempts to create a new plan on or after D + 60 days Then the plan is created successfully and the API returns 201 with planId
Reverse Default — Immediate Eligibility Recalculation
Given a plan in Default state and an authorized manager initiates a reverse default action When the reversal is committed Then any cooldown tied to that default is cleared, eligibility is recalculated immediately, UI/API eligibility flags reflect the change without waiting for background jobs, and an audit event default.reversed with actor, timestamp, and reason is recorded Given a reverse default is attempted on a plan that is not in Default state When the reversal is requested Then the action is blocked and the API responds 409 with code = INVALID_STATE and no eligibility changes occur
UI and API Enforcement — Consistent Blocking and Explanations
Given any guardrail violation occurs during create payment plan, grant fee waiver, approve partial/first payment, re-enroll after default, or reverse default When the action is attempted via UI and via API Then both layers prevent the action with identical machine-readable codes in {GUARDRAIL_MIN_FIRST_PAYMENT, GUARDRAIL_COOLDOWN_ACTIVE, GUARDRAIL_MAX_PLANS_PER_YEAR, GUARDRAIL_WAIVER_REQUIRES_CURRENT} and consistent human-readable messages including contextual values (e.g., percent, computed amount, or cooldown end date formatted as <MMM DD, YYYY>) And the API returns HTTP 409 with a stable schema: { code, message, metadata } And the UI displays an inline banner at the point of action and disables the confirm/submit control And no side effects (no records created/updated) occur on blocked attempts, and a guardrail.blocked audit event is recorded
Background Eligibility Updates and Notifications on Cooldown Expiry
Given a household H with an active cooldown ending at T When T is reached Then eligibility for the affected action(s) flips to eligible within 15 minutes without user intervention, and the UI reflects the enabled action state And an in-app notification is sent to H and an email is sent if email notifications are enabled, with message: "You are eligible to start a new payment plan" including the relevant bill/plan reference And exactly one notification is sent per cooldown expiry event (idempotent), even if the background job runs multiple times And the API eligibility endpoint reflects the updated eligible = true state within 15 minutes
Exception Justification & Approvals
"As a compliance lead, I want exceptions to require a documented reason and approval so that any policy overrides are controlled, transparent, and defensible."
Description

Enables authorized users to grant exceptions with required justification while keeping the system audit‑ready. Overrides must capture reason category, free‑text rationale, attachments (optional), and scope (single invoice/plan, household, or time‑bounded). Supports configurable approval workflows (e.g., second approver required for fee waivers above $X) and records who requested, who approved, timestamps, and which rule(s) were bypassed. Exceptions are visible in context, expire automatically if time‑bounded, and trigger notifications to relevant roles. Prevents silent overrides and ensures consistent, documented handling when policy exceptions are necessary.

Acceptance Criteria
Mandatory Justification on Exception Request
Given an authorized user initiates an exception to bypass a guardrail When they open the Request Exception form Then the form requires selection of a Reason Category from the configured list And the form requires entry of a free-text Rationale with at least 15 characters And attachments are optional but, if provided, must meet configured file type and size limits And the Submit/Request button remains disabled until all required fields are valid And attempting to submit with missing or invalid inputs shows inline errors and no exception record is created
Scoped Exception Application
Given an approved exception with scope Single Invoice/Plan When the guardrail would normally block or alter that invoice/plan Then the exception applies only to that specific object and no other household items are affected Given an approved exception with scope Household When the guardrail evaluates any applicable invoice/plan for that household within the active window Then the exception applies to all such items for that household Given a time-bounded scope with start and end When current time is outside the window Then the exception is inactive And the system prevents end timestamps earlier than start timestamps
Configurable Multi-step Approval for High-Impact Waivers
Given a configured rule requiring a second approver for fee waivers above $X When a requester submits an exception meeting or exceeding the threshold Then the exception status becomes Pending Approval (2 steps) And the requester cannot self-approve And only users with the Approver role can approve And the second approver must be different from the first And each approval captures approver identity and timestamp And if any approver rejects, the status becomes Rejected and the exception is not active Given the amount is below $X When submitted Then a single approval is sufficient to activate the exception
Audit Trail and Contextual Visibility
Given any exception is created, updated, approved/rejected, or expires When viewing the audit log for the related entity (invoice/plan/household) Then the log shows requester, approver(s), timestamps, rule(s) bypassed, reason category, rationale, scope, amount/impact (if applicable), and attachment metadata And audit entries are immutable; edits create new audit entries without altering prior records And in-context indicators are visible on affected invoice/plan/household pages linking to the exception details
Automatic Expiration and Re-enforcement of Guardrails
Given a time-bounded exception with an expiration timestamp When the expiration timestamp is reached Then the exception automatically transitions to Expired And original guardrails are re-applied to subsequent evaluations And an audit entry is recorded for the expiration event And configured roles are notified of the expiration Given a time-bounded exception is extended by an approver When the new end time is saved Then the prior end time remains in the audit history and the new end time becomes effective immediately
Role-based Notifications on Exception Lifecycle
Given notifications are configured for exception events When an exception is submitted Then designated approvers receive a notification with deep links to approve or decline When an exception is approved or rejected Then the requester and watchers receive notifications with the outcome, approver identity, and any provided rationale When an exception is modified or expires Then subscribed roles receive notifications Notifications respect user delivery preferences (in-app/email) and are not sent for saved drafts
Prevent Silent Overrides (UI and API)
Given a guardrail would prevent an action When a user attempts the action without an active approved exception Then the system blocks the action with a clear error referencing the guardrail and a path to Request Exception And the API returns a 4xx error with a machine-readable code and guardrail identifier Given an active approved exception exists for the relevant scope When the same action is attempted Then the system allows the action and associates the action with the exception ID in the audit trail
Rule Versioning & Change History
"As a board secretary, I want versioned guardrails with diffs and rollbacks so that we can explain past decisions and recover quickly from misconfigurations."
Description

Maintains a complete, immutable history of rule definitions with version numbers, authors, timestamps, effective windows, and change reasons. Provides readable diffs between versions and the ability to stage, schedule, and rollback to a prior configuration. Every eligibility decision stores a hash/snapshot of the active rule set so past decisions can be explained exactly as they were at the time. Integrates with audit logs and reporting so investigators can correlate outcomes with the precise policy in effect.

Acceptance Criteria
Create New Rule Version With Metadata
Given a policy admin with edit permissions When they save a new draft of Fairness Guardrails Then the system assigns version_number = previous_version_number + 1 And records author_user_id, author_display_name, and org_id And records created_at and updated_at timestamps in UTC (second precision) And requires a non-empty change_reason with a minimum of 10 characters And marks the draft with status = "Draft" and not effective until published
Publish, Schedule, and Effective Windows
Given a draft version that passes schema and rule validation When the admin publishes with effective_from (UTC) and optional effective_to (UTC) Then the system validates the window [effective_from, effective_to) does not overlap any published/scheduled version And enforces at most one Active version at any point in time And if effective_from > now, set status = "Scheduled"; if effective_from <= now, set status = "Active" And auto-transition to Active at effective_from without manual action And auto-transition to Expired at effective_to (if provided)
Immutable History and Diff View
Given two existing versions vA and vB When a user requests a diff between vA and vB Then the UI renders added/removed/modified rules with field-level highlights and change counts by type And the API returns structured JSON with arrays: added[], removed[], modified[] including rule identifiers and changed fields And attempting to edit or delete any Published/Active/Expired version returns HTTP 409 and logs an audit event type = "immutable_violation"
Rollback to Prior Configuration
Given a prior version vX exists and vCurrent is Active When an admin selects "Rollback to vX" and confirms with a change_reason Then the system creates a new version vN+1 whose payload exactly matches vX And sets vN+1.effective_from = now (or provided schedule) with status = Active or Scheduled accordingly And sets vCurrent.effective_to = vN+1.effective_from and transitions vCurrent to Expired at that instant And writes an audit log event event_type = "rollback" linking vN+1 and vX with actor_id and timestamp
Decision Snapshot and Hash Persistence
Given an eligibility decision is evaluated for a household When the decision record is created Then it stores rule_version_id, rule_set_hash = SHA-256(canonicalized rule payload), and a full JSON snapshot of the rules used And retrieving the decision returns these fields unmodified And recomputing SHA-256 over the stored snapshot equals the stored rule_set_hash And subsequent rule changes do not alter the stored snapshot or hash
Audit Log and Reporting Correlation
Given lifecycle events occur (create_draft, publish, schedule, activate, expire, rollback, immutable_violation) When any such event is processed Then an audit entry is written with fields: event_type, rule_version_id, actor_id, timestamp (UTC), change_reason (nullable), previous_version_id (nullable), correlation_id And reporting endpoints can filter eligibility decisions by rule_version_id, date range, and outcome, and export CSV And audit entries and decisions cross-reference via rule_version_id and correlation_id for investigators
Audit Trail & Exportable Reports
"As an auditor, I want exportable, time‑stamped decision logs and evidence packs so that I can verify equal treatment and confirm that exceptions were appropriate."
Description

Aggregates a time‑stamped log of rule evaluations, blocked actions, successful actions, exceptions, and approvals with link‑backs to households, invoices, plans, and rule versions. Provides filters (date range, community, action type, rule ID, outcome, user) and export to CSV/JSON for sharing with auditors or the board. Generates “evidence packs” that bundle the decision rationale, rule snapshot, and relevant ledger state at the time of action. Supports data retention controls per community policy and secure sharing links with expiration.

Acceptance Criteria
Time‑Stamped Rule Evaluation Log with Entity Link‑Backs
Given any rule evaluation (allow, block, exception, approval) occurs When the evaluation completes Then the system writes an immutable audit entry with ISO 8601 UTC timestamp, unique event_id, action_type, rule_id, rule_version, outcome, actor_id (user/service), and links to household_id, invoice_id (if any), and plan_id (if any) Given a log entry is created When retrieved via API or UI Then all link‑backs resolve to the correct entities and versions and the entry content is read‑only Given concurrent evaluations occur When entries are written Then event_id values are unique and ordering by timestamp is consistent to at least 1 ms precision Given a transient write failure occurs When retry logic executes Then at‑least‑once persistence is achieved without duplicate event_ids
Multi‑Parameter Filtering of Audit Log
Given audit entries exist across multiple communities and dates When filtering by date range, community_id(s), action_type(s), rule_id(s), outcome(s), and user/actor_id(s) Then only entries matching all specified filters are returned and results are sorted by timestamp desc by default Given an invalid date range (start > end) or malformed filter value When the query is submitted Then a client‑visible validation error is returned and no search is executed Given 10,000 matching entries When the first page is requested (page size 100) Then the response returns within 2 seconds and includes pagination metadata (total_count, page_size, cursor/offset) Given no filters are set When the log is opened Then entries default to the last 30 days for the current community
Export Filtered Audit Log to CSV/JSON
Given filtered audit results are displayed When Export CSV is requested Then the downloaded file contains exactly the filtered records with headers: event_id,timestamp_utc,community_id,action_type,rule_id,rule_version,outcome,actor_id,household_id,invoice_id,plan_id; encoding is UTF‑8; newlines are LF; and values are RFC 4180 compliant Given filtered audit results are displayed When Export JSON is requested Then the downloaded file contains an array of objects with the same fields and valid JSON syntax Given more than 100,000 records match When export is requested Then export streams without UI timeouts and completes successfully, and the file metadata (record_count, generated_at_utc, filter_summary) matches the UI Given an export completes When the file is validated Then the record count equals the UI‑reported count and random sample records match field‑for‑field
Generate Evidence Pack for a Decision
Given a decision/action event_id exists When Generate Evidence Pack is requested by an authorized role Then a bundle is created containing: decision rationale at decision time, rule snapshot (definition and version), and ledger state snapshot as of the event timestamp Given an evidence pack is generated When it is downloaded Then it is delivered as a single archive with a manifest.json including event_id, created_at_utc, generated_by, and SHA‑256 checksums for each included file Given a rule has changed since the decision When the evidence pack is opened Then it contains the original rule snapshot, not the current rule Given an action has no applicable invoice or plan When the evidence pack is generated Then the manifest marks those components as not_applicable and the archive omits their files Given an unauthorized user attempts generation When the request is made Then the system returns 403 and no pack is created
Retention Policy Enforcement for Audit Data and Evidence Packs
Given a community retention policy defines audit_entry_retention_months and evidence_pack_retention_days When the scheduled retention job runs Then entries and packs older than their respective limits are purged or archived per policy and are no longer retrievable via UI/API Given a legal hold is applied to a household_id or rule_id When the retention job runs Then affected audit entries and evidence packs are excluded from purge until the hold is removed, and the skip is logged Given retention settings are updated When the change is saved Then a dry‑run report of items pending purge is generated and, upon confirmation, the purge executes and is logged with counts by type Given an export is generated near its retention limit When the limit is reached Then any associated secure links are invalidated and the asset is removed or archived per policy
Secure Sharing Links with Expiration and Access Logging
Given a user generates a sharing link for an export or evidence pack When the link is created Then it contains a non‑guessable token, an explicit expiration timestamp, and scope limited to the specific asset Given the link is accessed before expiration When the token is presented Then the asset is downloadable per link configuration (requires login if enforced; otherwise token‑based) and the access is logged with timestamp, IP, user_agent, and user_id (if authenticated) Given the link is accessed after expiration or is revoked When the token is presented Then the system returns 410 (expired) or 403 (revoked) and logs the attempt Given a policy of max 5 active links per asset When a 6th link is requested Then link creation is blocked with a validation error or the oldest active link is auto‑revoked per configuration and the action is logged Given repeated invalid token attempts (>10 within 5 minutes) When requests continue Then rate limiting is applied and subsequent requests are temporarily blocked
Policy Simulation & What‑If Analysis
"As a board treasurer, I want to simulate policy changes so that we can assess fairness and operational impact before rollout."
Description

Allows admins to model proposed rule changes against historical and current data to estimate impact before publishing. Produces metrics such as number of households newly eligible/ineligible, expected changes in on‑time payments, anticipated exception volume, and which households would be most affected. Runs in a safe, non‑mutating environment and supports side‑by‑side comparison of multiple drafts. Results can be shared as a link or export to inform board decisions and reduce unintended consequences.

Acceptance Criteria
Run simulation on a draft policy
Given an admin with Manage Policies permission and a saved draft that changes eligibility or payment plan limits And a data window is selected (e.g., last 12 months) with baseline set to Current Policy When the admin clicks Run Simulation Then the simulation completes without error within 60 seconds for communities with ≤5,000 households And the results show: newly eligible households (count), newly ineligible households (count), projected on-time payment change (absolute and percent), anticipated exception volume (count), and a list of the 10 most affected households with reason codes And all totals equal the sum of their displayed breakdowns
Data selection and baseline controls
Given an admin opens the simulation setup When they choose a historical window between 1 and 36 months and optionally include current month-to-date And they select a baseline of Current Policy or a specific draft Then the form validates required fields and prevents Run if selections are incomplete And defaults are prefilled to last 12 months and Current Policy And the chosen window and baseline are persisted to the results and the shareable view
Side-by-side comparison of multiple drafts
Given at least two drafts exist When the admin selects up to 3 drafts and runs a comparison Then the comparison view displays each draft’s metrics alongside baseline values and delta (absolute and percent) And metric deltas use the same denominator and rounding rules across drafts And the admin can switch the baseline without rerunning calculations And exporting the comparison preserves the same numbers and draft labels
Safe, non-mutating simulations
Given a simulation is run Then no live data is mutated: 0 rows are inserted/updated/deleted in policy, billing, reminder, payment, and waiver tables And no emails, texts, or in-app notifications are sent or queued And no enforcement or billing jobs are scheduled And only a simulation record and metrics are stored in a simulations namespace with a read-only link
Shareable results link and export
Given a completed simulation When the admin clicks Share Then a view-only URL is created with a random token, expiring in 30 days by default, and can be revoked And viewers with the link can see results but cannot edit drafts or rerun simulations unless they have Manage Policies And the admin can export a CSV (per-household impacts and metrics) and a PDF summary And exports generate in ≤30 seconds and match on-screen totals and counts
Auditability and reproducibility
Given a completed simulation Then the results display: policy changes applied, data window, run timestamp (UTC), dataset version, simulator version/hash, and baseline used And each affected household entry includes reason codes and the rule(s) that triggered the impact And rerunning the same draft with the same inputs against unchanged data produces identical outputs And the simulation is saved with an immutable snapshot ID retrievable by URL

Fee Clarity

Show a plain‑language breakdown of principal, fees, and any interest, plus a side‑by‑side total cost if paying now vs. on the plan. Tap to expand an amortization view and accept terms digitally. Builds trust, reduces disputes, and reassures cautious payers before they commit.

Requirements

Plain-Language Cost Breakdown
"As a homeowner, I want a simple breakdown of what I’m being charged so that I understand exactly where my total comes from and feel confident paying online."
Description

Display a clear, itemized breakdown of principal, fees, interest, credits/discounts, taxes (if applicable), and total due for any invoice or post-turned-bill. The component pulls authoritative values from Duesly’s ledger/invoice services and shows calculation methods (e.g., APR, daily rate, fee basis) in plain language. It supports multi-charge posts, plan-specific adjustments, and live recalculation when the user toggles payment options or applies credits. Implement consistent currency formatting, rounding rules, and data validation with graceful fallbacks for missing components. Surface last-updated timestamps and source-of-truth indicators to increase trust. Delivered as a reusable UI module integrated into the payment flow, invoice detail, and announcement feed preview, with analytics events to measure engagement and dispute reduction.

Acceptance Criteria
Invoice Detail: Authoritative Itemized Breakdown
Given an invoice with principal, fees, interest, credits, taxes, and a ledger total When a user opens the Invoice Detail view Then the breakdown retrieves the latest values from Duesly’s ledger/invoice services and renders line items for each present component with plain-language labels And Total Due equals Principal + Fees + Interest + Taxes − Credits, matching the ledger-reported total to within $0.01 after rounding And interest/fee line items display their calculation method in plain language (e.g., “Interest calculated at 12% APR (0.0329% daily)” or “Late fee: flat $25 based on missed due date”) And the component shows a “Source: Ledger” indicator and a “Last updated” timestamp derived from the ledger record in the user’s local time zone And all amounts use locale-aware currency formatting and the platform’s rounding rules
Payment Flow: Live Recalculation on Plan Toggle and Credits
Given an invoice eligible for both Pay Now and a Payment Plan and a user with available credits/discounts When the user toggles between Pay Now and Payment Plan Then the component recalculates principal, fees, interest, plan adjustments, and Total Due within 500 ms after data is available And plan-specific fees/interest (e.g., setup fee, estimated interest through first installment) are itemized with plain-language labels When the user applies or removes a credit/discount Then a “Credits” line item appears or updates, the Total Due changes accordingly, and Total Due never drops below $0.00 And an analytics event "breakdown_option_changed" is emitted with {invoice_id, option, previous_option, credits_applied, totals}
Feed Preview: Quick Breakdown for Post‑Turned‑Bill
Given a post converted into a bill When viewing it in the announcement feed Then the card shows Total Due, due date, and a “View breakdown” affordance When the user taps “View breakdown” Then a preview displays present line items, Total Due, “Source: Ledger”, and “Last updated” without navigating away When the user taps “Pay” from the preview Then the payment flow opens with the same invoice context and any selected option preserved And analytics events "breakdown_preview_opened" and "breakdown_preview_pay_clicked" are emitted with {invoice_id, source:"feed"}
Graceful Fallbacks and Data Validation
Given any component (fees, interest, taxes, credits) is not applicable or absent in ledger data When rendering the breakdown Then only present components are shown; absent ones are omitted; Total Due still matches the ledger total within $0.01 after rounding When ledger/invoice services return partial data Then the UI shows a non-blocking inline message “Some details are temporarily unavailable” with a Retry action and suppresses calculation methods for missing items When data fetch fails Then a skeleton loader appears for up to 3 seconds, followed by an error state with “Retry” and “Contact manager”; analytics event "breakdown_load_failed" is emitted with error_code And all displayed amounts pass numeric validation; any invalid item is not rendered and triggers analytics event "breakdown_validation_failed"
Multi‑Charge Posts: Grouping and Aggregation
Given a post with multiple charges tied to a single invoice When rendering the breakdown Then the component groups amounts by charge with per-charge subtotals and a grand Total Due And taxes, fees, and credits specific to a charge appear under that charge; invoice-level credits appear under “Credits (invoice-wide)” And charges are ordered by due date ascending; within a charge: Principal, Fees, Interest, Taxes, Credits And the sum of all charge subtotals minus credits equals the displayed grand Total Due to within $0.01 after rounding
Currency Formatting, Rounding, and Labels
Given USD locale When rendering any monetary amount Then values are formatted with $ symbol, thousands separators, and 2 decimal places using round half up And negative amounts (credits/discounts) show a leading minus sign and a clear label (e.g., “Credit”) And zero-value optional line items are not displayed (except Total Due, which may be $0.00) And the sum of displayed line items equals the displayed Total Due after applying the same rounding rules; discrepancies > $0.01 emit analytics event "breakdown_sum_mismatch" and show a non-blocking warning icon
Side-by-Side Total Cost Comparison
"As a cautious payer, I want to see what I’ll pay in total if I pay now versus a plan so that I can choose the most affordable option for my situation."
Description

Provide a real-time, side-by-side comparison of “Pay in Full Now” versus “Enroll in Plan,” including total cost over time, monthly/instalment amount, plan duration, next due date, and estimated savings or cost difference. The comparison uses the same calculation engine as the ledger to include all relevant fees, interest, and discounts, and it updates instantly when the user changes start date, autopay, or applies credits. Display key assumptions and disclaimers, and handle edge cases such as waived fees, prepayment, and differing processor fees. Integrate seamlessly into Duesly’s checkout and plan selection UI to drive informed decisions and reduce buyer’s remorse.

Acceptance Criteria
Real-Time Update on Input Changes
Given the side-by-side comparison is visible When the user changes the plan start date Then total cost, monthly/installment amount, plan duration, next due date, and savings/cost difference update within 500ms to reflect the new start date Given the side-by-side comparison is visible When the user toggles Autopay on or off Then total cost, monthly/installment amount, and savings/cost difference update within 500ms to include/remove any autopay-related discounts or fees, and the assumptions text updates accordingly Given the side-by-side comparison is visible When the user applies or removes available credits Then both Pay Now and Plan totals recalculate within 500ms to reflect credits, and the remaining balance is accurate to the cent Given plan options of varying durations exist (e.g., 3, 6, 12 months) When the user selects a different plan duration Then the monthly/installment amount, plan duration, next due date, and plan total recalculate within 500ms and remain internally consistent
Ledger Parity for Totals and Line Items
Given a charge with principal, fees, interest, discounts, credits, and processor fees configured When the side-by-side totals are computed Then each displayed component and total for both Pay Now and Plan matches the ledger engine output for the same inputs within $0.01 Given currency and locale formatting settings are applied (USD, en-US) When amounts are displayed Then values are formatted to two decimal places with thousands separators and a leading currency symbol Given the amortization view is expanded for the selected plan When the schedule is rendered Then the sum of principal, interest, and fees across all installments equals the displayed plan total within $0.01 and the next due date matches the first unpaid installment date
Credits, Discounts, and Waived Fees Handling
Given a fee is flagged as waived in the ledger When the comparison is rendered Then the fee line shows as Waived with $0.00 and the waived amount is excluded from all totals and differences Given a discount with an expiration date exists When the user selects a plan start date after the discount expiration Then the discount is not applied, totals reflect $0.00 discount, and an inline note indicates discount not applicable Given available credits exceed or equal the total due When the user applies credits Then the Pay Now total becomes $0.00 and the Plan option is disabled or shown as $0.00 with a 0-month duration; the Enroll in Plan CTA is disabled Given partial credits are applied When totals are recalculated Then both sides decrease by the credited amount and any percentage-based fees are recalculated on the new subtotal
Prepayment Impact in Plan View
Given the amortization view is expanded When the user selects a Prepay Remaining Balance on [date] earlier than the plan end date Then the plan total recalculates using interest accrued through the selected payoff date (plus any configured early payoff fee) and the savings/cost difference updates accordingly within 1s Given the plan configuration has no prepayment penalty When the user selects a prepayment date Then no prepayment penalty is added to the plan total and the assumptions text indicates No prepayment penalty Given the prepayment date equals the plan start date When totals are recalculated Then the plan total equals the Pay Now principal plus any plan enrollment/processor fees and labels indicate Early payoff at start
Processor Fee Differences by Payment Method
Given the user selects Card for Pay Now and ACH Autopay for the Plan When totals are recalculated Then the Pay Now total includes card processor fees, the Plan total includes ACH-related fees (if any), and the cost difference reflects these differences within $0.01 Given the user switches Pay Now payment method from Card to ACH When the selection changes Then processor fees update within 500ms and the cost difference re-evaluates; if ACH has $0 fees, a No processor fee with ACH note is shown Given the plan requires ACH Autopay only When the user attempts to select Card for the plan Then the selection is rejected with a brief inline message and totals remain based on ACH
Assumptions and Disclaimers Visibility and Accuracy
Given the side-by-side comparison is visible When rendered Then an Assumptions section displays interest/fee model, APR or fee schedule, autopay requirement/discount, processor fee basis, late fee policy, prepayment policy, enrollment fee (if any), and calculation date/time Given the user changes Autopay, payment method, or start date When the change is applied Then the Assumptions text updates within 500ms to remain accurate to the current configuration Given the user opens Terms & Disclosures When the dialog appears Then the Enroll in Plan CTA is disabled until the user checks I agree; upon agreement, the CTA enables and the timestamped consent is captured
Checkout Integration and CTA Enablement
Given the checkout/plan selection screen is loaded When the side-by-side component renders Then it appears above the primary CTAs, matches the design system, and is keyboard- and screen-reader accessible Given the user selects Pay Now or Enroll in Plan When the selection changes Then the corresponding CTA label/state updates immediately, the other option is deselected, and the selection persists on navigating back/forward in checkout Given calculation results are loading or an error occurs When the user views the CTAs Then CTAs are disabled, a loading skeleton or inline error message is shown, and a Retry action re-attempts calculation; on success, CTAs re-enable Given viewport widths of 320px to 1440px When the component is rendered Then both columns remain readable without horizontal scroll, with a vertical stack applied below 480px
Tap-to-Expand Amortization View
"As a resident considering a payment plan, I want to see the full schedule and how each payment is applied so that I know exactly what to expect before I commit."
Description

Offer an expandable amortization view that reveals the full payment schedule for selected plans, including due dates, payment amounts, principal versus interest allocation, remaining balance after each payment, and any per-instalment fees. Support dynamic recalculation when plan parameters change, and provide export options (CSV/PDF) for records. Optimize for mobile with virtualized scrolling and clear date/amount formatting. The view consumes plan templates and schedule data from Duesly’s billing services and respects user locale and timezone settings. This transparency builds trust and reduces support tickets around schedule expectations.

Acceptance Criteria
Expand/Collapse Amortization Schedule
Given a plan card with an amortization toggle, when the user taps Expand, then the view displays the full schedule with columns: Payment #, Due Date (locale-formatted), Payment Amount (localized currency), Principal, Interest, Per‑instalment Fee, Remaining Balance. Given the schedule is expanded, when the user taps Collapse, then the schedule hides and focus returns to the originating plan card without altering selected plan parameters. Given a schedule with N payments, when expanded, then the header shows the plan name, total number of payments (N), and total cost; the final row shows Remaining Balance = 0.00 in the plan currency. Given all rows are rendered, then sum(Principal) + sum(Interest) + sum(Per‑instalment Fee) equals sum(Payment Amount) within ±0.01 of the currency minor unit.
Dynamic Recalculation on Parameter Change
Given the schedule is expanded, when the user changes start date, number of instalments, down payment, interest rate, or per‑instalment fee and the billing service returns a recalculated schedule, then the view updates within 500 ms of the response and reflects updated due dates, amounts, allocations, and balances. Given a recalculation is in progress, then a loading indicator is shown and stale row data is not displayed; updated rows replace prior rows atomically to avoid mixed states. Given the billing service returns a validation error for the submitted parameters, then the schedule remains unchanged and an inline error message is displayed with a retry option and the invalid fields indicated. Given the user changes locale or timezone settings, when the schedule is recalculated or re-rendered, then dates and currency are reformatted accordingly without altering monetary totals.
CSV and PDF Export of Schedule
Given the schedule is expanded, when the user taps Export CSV, then a CSV downloads with a header row and one row per payment containing: payment_number, due_date_iso, due_date_display, payment_amount_minor_units, principal_minor_units, interest_minor_units, per_instalment_fee_minor_units, remaining_balance_minor_units, currency_code, timezone. Given the schedule is expanded, when the user taps Export PDF, then a paginated PDF is generated with table headers repeated each page, all rows included, and totals shown; amounts and dates are formatted per the user’s locale. Given an export completes, then the file name follows "duesly_schedule_{planId}_{YYYYMMDD}.{csv|pdf}" using the user’s timezone date. Given a schedule with more than 200 rows, when exporting CSV or PDF, then the file is produced within 5 seconds on a mid‑tier mobile device and contains all rows without truncation.
Locale and Timezone Respect
Given a user with locale en‑US and timezone America/Denver, when the schedule renders, then dates display as MM/DD/YYYY in Mountain Time and currency uses $ with comma thousand separators and 2 decimals. Given a user with locale de‑DE and timezone Europe/Berlin, when the schedule renders, then dates display as DD.MM.YYYY and currency uses € with period thousand separators and comma decimals. Given a due date occurs during a daylight saving time transition, when displayed, then the due date stays on the intended local calendar day at 00:00 and does not shift to an adjacent day. Given the service provides UTC timestamps, then the view converts to the user’s timezone consistently for all rows and for export metadata.
Mobile Virtualized Scrolling Performance
Given a schedule with 500 or more payments, when continuously scrolling on a mid‑tier mobile device, then average frame render time stays under 16.7 ms with no more than 1 dropped frame per second. Given virtualization is enabled, then only visible rows plus a buffer of up to 20 rows are mounted at any time; the amortization view’s memory footprint remains under 100 MB. Given the user performs a fast scroll or jump to index, then the target rows render within 300 ms and the header remains visible. Given any interactive row element, then the tap target height is at least 44 px and is reachable via VoiceOver/TalkBack with correct focus order and labels.
Data Consumption and Error Handling from Billing Services
Given a plan is selected, when the amortization view loads, then it requests the schedule and template data from Duesly billing services using the current planId and environment with at most one network round trip per expansion. Given the billing service response omits any required schedule field, then the view blocks expansion, shows a clear error stating the missing fields, and provides a retry action; no partial data is shown. Given the network request fails due to timeout (≥10 s), 5xx, or offline, then the view displays a non‑blocking error with Retry and logs telemetry including error code and correlationId. Given the billing service version is incompatible, then the view informs the user to refresh or update and links to support; no schedule is displayed until compatibility is restored.
Digital Terms Acceptance & Timestamp
"As a board member, I want residents to accept clear terms with a recorded proof of consent so that the HOA is protected and disputes are minimized."
Description

Require explicit digital acceptance of plan terms, fee policies, and authorization language before enrollment or payment submission. Present a concise summary with the ability to expand to full terms, link to HOA documents, and support accessibility and localization. Record legally relevant metadata (timestamp, user ID, IP address, device, terms version) and store an immutable receipt accessible to admins and the payer. Email/receipt the accepted terms and surface them in the payment confirmation. Integrate with Duesly’s existing identity and audit layers to satisfy compliance and reduce disputes.

Acceptance Criteria
Consent Gate Before Enrollment or Payment
Given a logged-in payer is on the plan enrollment or payment submission screen When the “I agree to the plan terms, fee policies, and authorization” checkbox is unchecked Then the primary action (Enroll/Pay) is disabled and a helper message states acceptance is required Given the payer selects “Review terms” When the terms modal opens Then a concise summary is displayed with an “Expand full terms” control and working links to referenced HOA documents Given the payer checks the acceptance box and selects “Accept & Continue” When the modal is closed Then the primary action becomes enabled for this session And the displayed termsVersion is bound to the impending enrollment or payment
Legal Metadata Capture on Acceptance
Given a payer accepts the terms Then the system records metadata: timestamp (UTC ISO-8601 with milliseconds), userId, accountId/communityId, payer name and email, IP address, user agent/device, sessionId, termsVersion, referenced hoaDocumentVersionIds, locale/language, and consentMethod=checkbox And the acceptance record is atomically written to the audit store and associated with the paymentId or planEnrollmentId And the acceptance record is retrievable by admins and the payer without exposing sensitive tokens Given the acceptance metadata is stored When an admin views the audit trail Then all fields are visible and the timestamp is normalized to UTC and displayed in the admin’s local time with offset
Immutable Terms Receipt and Confirmation
Given acceptance is captured and the payment or enrollment completes When the system generates a receipt Then the receipt includes the acceptance text snapshot, metadata (including termsVersion and hoaDocumentVersionIds), a unique receiptId, and a SHA-256 hash of the snapshot + metadata And the receipt is stored as read-only; any update attempt is rejected and logged Given the transaction completes Then the confirmation screen displays a “Terms accepted” summary with the termsVersion and a link to view/download the full receipt (PDF/HTML) And the payer receives an email with the receipt within 5 minutes of completion And admins can access the same receipt from the transaction detail
Accessible, Localized Terms Presentation
Given the payer uses a screen reader or keyboard-only navigation When the terms summary/modal is opened Then focus moves into the modal, all controls are keyboard reachable in logical order, focus is trapped until dismissal, and the modal announces its role/title And the UI meets WCAG 2.2 AA for color contrast (>=4.5:1), visible focus, and text resizing to 200% without loss of content or functionality Given the payer’s preferred locale is supported When the terms are displayed Then all terms UI strings and the terms content appear in that language (including RTL where applicable) And the locale used is recorded in the acceptance metadata And if the locale is not supported, English is shown with a notice and recorded
HOA Document Linking and Versioning
Given the terms summary references HOA documents When the payer selects a document link Then it opens in a new tab/window and displays the exact version referenced by the terms And the acceptance record captures the specific hoaDocumentVersionIds Given HOA documents or terms are updated When a new version is published Then a new termsVersion is served to subsequent payers, and prior acceptances remain bound to their original versions And the UI displays the termsVersion to the payer at acceptance time
Identity and Audit Layer Integration
Given a user is not authenticated When they attempt to enroll or submit a payment Then they are redirected to authenticate and returned to the same step; acceptance cannot be captured anonymously Given an admin initiates a payment on behalf of a payer When the payment requires terms acceptance Then the system requests acceptance from the payer via a secure link or in-session if the payer is present and authenticated And the acceptance record’s userId must match the payer’s identity; admins cannot accept on behalf of the payer Given acceptance is recorded Then the audit trail shows the accepting userId, accountId/communityId, and a cross-reference to the transaction/event id
Failure Handling and Resilience
Given the acceptance audit write fails When the payer attempts to submit enrollment or payment Then the submission is blocked, no funds are captured, and an actionable error message is shown with a retry option Given email delivery of the receipt fails When the transaction completes Then the on-screen confirmation still shows the receipt link, the email is queued for retry up to 3 times over 24 hours, and the failure is logged for admins Given IP or device data cannot be captured due to network or privacy settings When acceptance is recorded Then null-safe defaults are stored with reason codes, and the acceptance is not blocked
Contextual Fee Definitions & Tooltips
"As a resident unfamiliar with HOA fees, I want quick explanations of each line item so that I don’t have to contact support to understand my bill."
Description

Embed inline info icons and tooltips next to each cost component that explain in plain language what it is and how it’s calculated. Provide a “Learn more” drawer with HOA-configurable policy text, examples, and links to governing documents. Allow admins to customize fee labels and descriptions in Duesly settings and localize content per community language needs. This reduces confusion, lifts self-service understanding, and lowers support burden.

Acceptance Criteria
Inline Tooltips Presence & Content
Given a payer is viewing the Fee Clarity breakdown for a bill When the breakdown renders Then each visible cost component (e.g., Principal, Fees, Interest) has an adjacent info icon with an accessible label "About [component label]" And activating the icon reveals a tooltip within 150 ms And the tooltip title equals the component label And the tooltip body contains sections labeled "What this is" and "How it's calculated" And the "How it's calculated" section references the same inputs and rates shown on the bill (amounts, dates, rate names) and matches their current values And if the bill’s calculation parameters change in-session, the tooltip content updates within 1 second to reflect the new values
Tooltip Interaction, Positioning, and Accessibility
Given a user interacts with an info icon via mouse, keyboard, or touch When the icon receives hover, focus, or tap Then the tooltip appears and remains visible until focus/hover ends or the user dismisses it And the tooltip can be dismissed via ESC, clicking/tapping outside, or moving focus And the trigger and tooltip are linked by aria-describedby and the tooltip has role="tooltip" And tooltip text is read in a logical order by screen readers And tooltip contrast ratio is at least 4.5:1 and the hit area of the icon is at least 24x24 dp And the tooltip auto-repositions to avoid obscuring the focused element or core figures and stays within viewport bounds
Learn More Drawer Content and Links
Given a tooltip for any cost component is open When the user selects "Learn more" Then a modal drawer opens within 300 ms with a visible title matching the component label And the drawer displays HOA-configured sections for Policy, Examples, and Governing Documents when provided And sections not configured are hidden without placeholders And content supports basic formatting (headings, lists, links) and renders safely without executing scripts And links open in an in-app web view or new tab using https only and display the destination domain And the drawer provides accessible close controls and returns focus to the originating trigger on close
Admin Customization of Labels and Descriptions
Given an HOA admin with permissions is in Duesly settings for Fee Clarity When the admin edits a component label and description Then validation enforces label length 1–40 chars, description length 1–500 chars, and disallows executable HTML/JS And the admin can preview tooltips and the Learn more drawer before saving And upon save, changes are versioned with timestamp and admin ID and a revert to default option is available per component And published changes propagate to payer-facing views within 2 minutes and are reflected in tooltips and the Learn more drawer
Localization and Fallback Behavior
Given a community default language and optional translations per component are configured When a payer whose profile language is X views Fee Clarity Then component labels, tooltip text, and Learn more content render in language X if available And if any translation is missing, the UI falls back to the community default; if still missing, it falls back to English And RTL languages render with correct text direction and mirrored UI affordances for tooltips and drawers And numbers and dates follow the viewer’s locale formatting while currency remains the community’s currency
Permissions, Security, and Auditability
Given a non-admin attempts to access Fee Clarity content settings When they try to edit labels, descriptions, or Learn more content Then the action is blocked with a 403-equivalent response and no changes are saved And all admin changes to Fee Clarity content are logged with timestamp, admin ID, fields changed, and previous/new values And URLs added to Governing Documents are validated to use https and reject data:, javascript:, and localhost schemes And stored content for tooltips and the drawer is sanitized server-side to strip scripts and unsafe HTML before rendering
Cost Calculation Audit Log
"As an administrator, I want a traceable record of how totals were calculated so that I can quickly resolve disputes and verify accuracy."
Description

Create structured, queryable logs for every cost calculation and display decision, capturing inputs (rates, fees, dates), formulas used, outputs, user-selected options, and versioned configuration at time of computation. Generate correlation IDs to trace an on-screen total back to ledger entries and gateway fees, with integrity checks to detect mismatches. Expose an admin-only viewer for dispute resolution and export logs as needed under the HOA’s data retention policy. Integrate alerts for anomalous discrepancies to maintain accuracy and trust.

Acceptance Criteria
Calculation Event Logging Completeness
Given Fee Clarity performs a cost computation (initial render or recalculation) When the computation completes Then a single audit log entry is written with a unique correlationId And it records: timestamp (UTC), requestId, userId or system actor, hoaId, accountId/unitId, currency, locale, device/timezone, inputs (principal, itemized fees, rates, dates, plan selection, waivers/discounts), formulas used (identifiers and expressions), configurationVersionId, outputs (totals, per‑installment amounts, interest, gateway fees, rounding), integrityCheckStatus And all required fields are non‑null and typed per schema; optional fields are present only when applicable And the entry is persisted within 200 ms p95 and retrievable by correlationId
Display Decision Logging Completeness
Given Fee Clarity renders a cost breakdown or amortization view When the user toggles pay‑now vs plan, expands/collapses amortization, changes plan terms, or accepts terms digitally Then an audit log entry is recorded referencing the current correlationId And it captures the UI state shown (line‑items displayed, totals, amortization presence), user‑selected options, terms acceptance metadata (timestamp, consent text version, signature hash, IP), and reason codes for hidden/omitted elements And the displayed numeric values exactly match the latest computed outputs for the correlationId (tolerance 0 currency units)
Versioned Configuration Snapshot at Computation Time
Given versioned fee schedules, interest rules, rounding policies, gateway fee tables, and tax rules exist When a cost computation runs Then the audit log stores an immutable snapshot reference including configuration version IDs and the literal formula expressions used And recomputing later using the stored snapshot reproduces outputs with 0 currency unit difference And modifying configuration versions after logging does not alter existing log entries or their snapshots
Correlation ID End-to-End Traceability
Given a cost total is displayed to a user When corresponding ledger entries and gateway fee calculations are generated Then the same correlationId is attached to the UI display log, calculation log, ledger records, and gateway fee record And querying by correlationId returns all related records within 1 second p95 And correlationId conforms to UUID v4 format and is globally unique with no duplicates across 10 million generated IDs
Integrity Checks and Anomaly Alerts
Given calculation outputs are compared to persisted ledger and gateway amounts When a mismatch > 0.01 currency units or > 0.1% relative difference is detected, or when required related records are missing Then the audit log integrityCheckStatus is set to 'Fail' with machine‑readable reason codes And an anomaly alert is emitted to the monitoring channel with correlationId and details within 30 seconds And the admin viewer displays a visible discrepancy banner on the affected log detail And upon resolution, the status transitions to 'Pass' with an immutable trail of the remediation event (actor, timestamp, notes)
Admin-Only Audit Log Viewer with Search and Filter
Given a user with Admin role accesses the audit viewer When they search by date range, hoaId, accountId, userId, correlationId, amount range, or integrity status Then results return within 1 second p95 with pagination and sortable columns And non‑admin users receive HTTP 403 with no data exposure And the detail view shows inputs, formulas, outputs, configuration snapshot, integrity status, and links to related ledger/gateway records And a Recompute action uses the stored snapshot to re‑run the calculation and displays a diff if any discrepancy is found And all viewer access and actions (filters, views, recompute, exports) are themselves audit‑logged with actor and timestamp And PII fields are redacted per policy in the viewer while remaining available in export to authorized admins
Export and Retention Compliance
Given an admin requests an audit log export When they specify date range, HOA scope, and format (CSV or JSON) Then the system generates a signed export containing selected fields, a data dictionary, and a SHA‑256 checksum And the export is available within 2 minutes for up to 1,000,000 rows and streamed for larger datasets And the export is encrypted at rest and in transit, and auto‑expires after 7 days And an export audit record is logged with requester, scope, row count, checksum, and artifact link And data retention policy is enforced: logs older than the configured period are excluded and purge jobs remove expired records with an audited summary
Rounding, Proration, and Edge-Case Handling
"As a finance volunteer, I want consistent and documented rules for edge cases so that totals match across views and reconciliation is straightforward."
Description

Define and implement consistent rounding (e.g., standard vs bankers), proration for mid-cycle starts, weekend/holiday due date adjustments, minimum payment constraints, fee caps, and late-fee waiver interactions. Ensure the rules are centralized in a shared calculation service used by ledger, checkout, and comparison views to prevent drift. Add comprehensive unit and integration tests for boundary scenarios (tiny balances, early payoff, partial payments, refunds, reversed fees) and document rules for support and compliance. This guarantees consistent totals across the product and reduces reconciliation issues.

Acceptance Criteria
Unified Calculation Service Parity Across Views
Given the calculation service is the single source for totals When ledger, checkout, and comparison views request calculations for the same inputs and timestamp Then principal, fees, interest, and totals match exactly to the cent across all views and API responses Given a new version of the calculation service is deployed When a request specifies a pinned version Then outputs match prior results; When no version is pinned Then the response includes the applied version and change log id Given an input is recalculated within the same minute window When idempotent keys are used Then the service returns identical results and an idempotency indicator
Rounding Policy Enforcement and Precision Controls
Given policy is Bankers and scale is 2 When computing line items and totals Then each amount is rounded using banker's rounding to 2 decimals and the total equals the sum of displayed rounded components Given policy is Standard and scale is 2 When the policy is switched Then outputs reflect standard rounding and the response payload includes policy=Standard and scale=2 Given storage precision is 4 and display precision is 2 When returning API responses Then both raw (4dp) and rounded (2dp) fields are present and the difference between summed raw and rounded totals is <= $0.01 Given a negative rounding drift is detected across installments When the final installment is generated Then the final installment is adjusted by a single cent to eliminate drift and a driftAdjustment flag is included
Proration for Mid-Cycle Plan Start
Given a 30-day cycle with plan start on day 10 When generating the first period Then fees and interest are prorated for 20 days and rounded per policy and the first installment amount respects minimum payment rules unless final-only exception applies Given proration yields an installment below the minimum When the schedule is built Then the sub-min amount is combined with the next installment or rolled into the final installment per policy and no orphan pennies remain Given the user pays mid-day in their community timezone When proration is computed Then the day-count uses community timezone cutoffs, not device timezone
Weekend and Holiday Due Date Adjustment
Given a scheduled due date falls on a weekend or configured holiday When generating the schedule Then the due date shifts to the next business day and no late fee or additional interest accrues for the non-business days Given the community timezone spans a DST transition near the due date When computing the cutoff Then end-of-day respects local civil time and does not double-charge interest Given the holiday calendar is updated by admins When recalculating future schedules Then only future due dates adjust and past posted items remain unchanged with an audit entry
Minimum Payment Constraints and Fee Caps
Given minimumPayment is $25 and feeCap is min($50, 5% of principal) When generating a multi-installment plan Then each installment except the final is >= $25 and total fees do not exceed the cap and the final installment may be < $25 only to clear residual Given a partial payment exceeds the scheduled installment When allocation is applied Then allocation order is fees then interest then principal and the remaining schedule rebalances without violating minimum payments or fee caps Given fees would exceed the cap due to compounding When calculating Then fees are clipped at the cap and the response includes a feeCapApplied indicator
Late-Fee Waiver Interaction and Non-Accrual
Given a late fee is waived before posting interest When recalculating totals Then the waived amount is excluded from interest accrual and from future schedules and the ledger records a waiver entry with actor, timestamp, and reason Given a waived fee is reinstated After a specified date When recalculating Then interest accrues prospectively from reinstatement date only and no retroactive interest is added Given a waiver is reversed in error When the reversal is reversed (double-reversal) Then the system returns to the prior state with identical totals and a complete audit trail
Edge-Case Handling, Test Coverage, and Documentation
Given the remaining balance is < $0.01 When recalculating Then the account is marked settled and no negative balances or additional charges are created Given an early payoff occurs before the next due date When applying the payment Then interest is prorated to the payoff timestamp and all future scheduled fees and interest are canceled Given a refund or fee reversal is posted When totals are recomputed Then UI, ledger, and API totals stay in sync within $0.01 and no duplicate accruals occur Given intermittent partial payments are received out of order When allocation runs Then allocation order is fees then interest then principal and the updated payoff date is communicated to all views Given unit and integration test suites run in CI When executed Then line coverage for the calculation service is >= 90% and branch coverage is >= 80% and all boundary-case tests (tiny balances, early payoff, partial payments, refunds, reversed fees) pass Given calculation rules change When the release is cut Then versioned support and compliance documentation is auto-generated and published within 24 hours including examples and policy identifiers

Segmented Broadcasts

Target the right owners across multiple communities with dynamic filters (dues status, roles, language, building, tags) and live recipient counts. Preview who’s in/out, simulate channel delivery, and avoid noisy, irrelevant messages. Result: cleaner inboxes, higher read rates, and fewer follow‑ups for managers.

Requirements

Cross-Community Segment Builder
"As a multi-community manager, I want to build precise recipient segments using filters like dues status, roles, language, and building so that I can target only the relevant owners across my communities."
Description

Provide a single UI to construct dynamic recipient segments across one or multiple communities using stackable filters (dues status, role, language preference, building/unit, tags, last payment date, account state). Support AND/OR/NOT logic, nested groups, include/exclude lists, and dynamic segments that auto-update as resident data changes. Integrate with Duesly’s Directory and Payments to ensure filter accuracy (e.g., overdue thresholds, partial payments, suspended accounts). Validate filters in real time, persist builder state, and allow saving, naming, and sharing segments with appropriate access controls. Ensure performance for communities up to mid-size scale with responsive interactions and graceful error handling. Expected outcome: managers can precisely target recipients without exporting data or manual list cleanup, improving message relevance and reducing noise.

Acceptance Criteria
Build Multi-Community Segment with Nested AND/OR/NOT Filters
Given I have access to multiple communities When I set the community scope to Communities A and B Then only residents from Communities A and B are eligible for the segment Given I add filters Role=Owner AND (DuesStatus=Overdue OR Language=Spanish) AND NOT Tag=DoNotContact with two nested groups When I apply the logic Then the live recipient count updates within 1 second and the preview first page matches the logic Given I create nested groups up to 3 levels deep When I save and reload the builder Then the exact group structure and filter values are restored Given I modify any filter value When I change a parameter Then the UI recalculates the count without a full page reload and indicates recalculation in progress
Directory and Payments Data Integration for Filter Accuracy
Given a resident is 15 days overdue with a partial payment recorded When I filter DuesStatus=Overdue>30 days Then that resident is excluded Given a resident account is Suspended in Payments When I filter AccountState=Suspended Then that resident is included Given I filter LastPaymentDate before 2025-06-01 When I preview results Then each included resident’s last payment date is before 2025-06-01 per the Payments ledger Given I compare UI counts to backend API results for the same predicate When the query completes Then the counts match exactly
Dynamic Segment Auto-Updates with Resident Data Changes
Given a saved segment defined as DuesStatus=Overdue>0 When a resident posts a payment that brings their balance to 0 Then they are removed from the segment and the live count updates within 60 seconds Given a new owner with Tag=DogOwner is added to a scoped community When the Directory sync completes Then they are added to any saved segment filtering Tag=DogOwner within 60 seconds Given a broadcast is scheduled using a saved segment When the send time is reached Then the segment membership is re-evaluated and a membership snapshot (IDs and count) is stored in the audit log
Include/Exclude Lists Override
Given a base segment returns N residents When I add Include list of 3 specific residents and Exclude list of 2, with one resident appearing in both lists Then the final recipient count equals N + 3 - 2 - 1 and contains no duplicates Given I attempt to include a resident outside the selected community scope When I add their ID or email Then the system blocks the addition and displays a validation message Given includes and excludes are configured When I open the preview Then the UI shows separate counts for Base, Included, Excluded and highlights included/excluded recipients in the list
Save, Name, and Share Segments with Access Controls
Given I have Manager permissions When I save a segment with a unique name Then it is persisted with owner, visibility, and last-modified metadata and appears in My Segments Given a segment name already exists within my organization When I attempt to save with the same name Then I am prompted to choose a different name and the save is blocked Given I set visibility to Organization-Shared When a user without ManageSegments permission tries to edit Then they can use the segment but cannot modify or delete it, and edits return 403 Given a user without access attempts to open a private segment When they navigate via a direct URL Then access is denied (403) and an audit event is recorded
Real-Time Filter Validation and State Persistence
Given I create an invalid expression (e.g., empty group, NOT without operand, unmatched parentheses) When I click Apply or Save Then an inline error message appears identifying the issue and the action is prevented Given I have unsaved changes When I attempt to navigate away Then I am prompted to confirm and, if I proceed, the draft is autosaved locally Given I return to the builder within 24 hours after leaving with unsaved changes When the page loads Then my draft state is restored exactly (scope, filters, groups, includes/excludes) Given a network interruption occurs during Save When connectivity resumes within 60 seconds Then the builder retries once and, if still failing, surfaces a non-technical error with a Retry option without losing state
Performance and Error Handling at Mid-Size Scale
Given an organization with up to 20 communities and 15,000 residents total When adding, removing, or editing a filter Then input latency is under 200 ms and the live count updates within 1 second for the 95th percentile of interactions Given I open the recipients preview (50 rows per page) When the query executes Then the first page renders within 1.5 seconds and subsequent pages within 500 ms Given a query runs longer than 10 seconds When the timeout is reached Then a friendly timeout message with Retry and Narrow Filters options is shown, no duplicate requests are issued, and the UI remains interactive Given any server or data error occurs When the error is handled Then a correlation ID is logged, the user sees a non-technical message, and no sensitive details are exposed
Live Recipient Count & Preview
"As a board member, I want to see a live count and preview of recipients included and excluded so that I can verify the audience before sending and avoid irrelevant messages."
Description

Continuously calculate and display live recipient counts as filters change, with a breakdown by community and deduplication across communities and units. Provide a privacy-aware preview of who is included and excluded (name, unit/building, community, primary delivery channel readiness) with pagination and sampling for large sets. Highlight reasons for exclusion (e.g., opted out, missing contact info, unverified email) and allow quick navigation to resolve data gaps. Enforce configurable max-segment safeguards and show estimate accuracy for counts. Expected outcome: senders gain confidence in audience size and composition, reducing accidental over-broadcasts and follow-up corrections.

Acceptance Criteria
Live, Deduplicated Recipient Count Updates on Filter Changes
- Given filters are applied to a segment, When the user adds, removes, or edits any filter, Then the total recipient count recalculates and renders within 500 ms of the last input (250 ms debounce) and reflects only unique people. - Given recipients exist across multiple communities and/or units, When counts are calculated, Then each person is counted once based on unique person_id (unit- and community-level duplicates are not double-counted) and a "Deduped" indicator shows the number removed. - Given the user rapidly changes filters, When multiple calculations are in-flight, Then the UI displays results for the most recent filter set only (no reversion to stale counts) and shows a calculating indicator while pending. - Given a successful calculation, When the count is displayed, Then a Last updated timestamp (UTC) is shown and aligns with the current filter state.
Community-Level Breakdown Consistency and Deduplication
- Given a deduplicated total count is displayed, When viewing the community breakdown, Then the sum of per-community counts equals the deduplicated total. - Given a person belongs to multiple selected communities, When breakdown attribution occurs, Then the person is attributed to a single community based on a deterministic tie-breaker (home_community if set; else lowest community_id) to preserve sum integrity. - Given the community breakdown is visible, Then each community row shows: community name, unique recipient count, and percentage of total; zero-count communities are hidden by default with a Show all toggle. - Given breakdown details are expanded for a community, Then the UI lists top applied filters contributing to inclusion for transparency.
Privacy-Aware Included/Excluded Preview with Pagination and Sampling
- Given included recipients ≤ 5,000, When the preview is opened, Then page 1 of the Included tab loads within 1 s, showing 50 rows per page with pagination controls. - Given included recipients > 5,000, When the preview is opened, Then the UI displays a randomized sample of 200 Included and 200 Excluded recipients with a Sampling notice and a Refresh sample action; full counts remain visible above the list. - Given any preview row, Then only the following fields are shown: display_name (or Household placeholder), unit/building, community, and primary delivery channel readiness; no email addresses or phone numbers are rendered. - Given Included and Excluded tabs, When switching tabs or pages, Then the table responds within 750 ms and preserves the current filter state. - Given search within preview by name or unit, When a query is applied, Then results return within 1 s and respect privacy constraints.
Primary Delivery Channel Readiness and Delivery Simulation
- Given an account-level channel priority is configured (e.g., App > Email > SMS > Print), When readiness is computed, Then each included recipient is assigned a primary channel per that order using only verified/valid contact methods. - Given readiness is computed, Then the UI shows channel counts (e.g., App-ready, Email-ready, SMS-ready) that sum to the deduplicated total included. - Given the user clicks Simulate delivery, When computation runs, Then channel-specific success/fail counts return within 2 s for N ≤ 10,000 with a status badge per channel; recipients without any deliverable channel appear in Excluded with reason no_deliverable_channel. - Given a recipient’s contact details are updated (e.g., email verified), When the preview is refreshed, Then channel assignment and counts update within 5 s to reflect the change.
Exclusion Reasons with Quick Resolution Navigation
- Given excluded recipients exist, When viewing the Excluded tab, Then each row displays a primary reason code from the standardized set {opted_out, missing_contact_info, unverified_email, invalid_phone, bounced, no_preferred_channel, suppressed_by_max_segment}. - Given a recipient has multiple exclusion reasons, Then the most severe reason is shown as primary with a tooltip listing all reasons. - Given the user clicks Fix on an excluded row, When navigation occurs, Then the relevant profile/settings screen opens in a new tab with return context to the segment builder. - Given the data gap is resolved, When the user returns to the segment builder, Then the recipient moves to Included and counts update within 5 s; an audit event is logged for the resolution action.
Configurable Max-Segment Safeguard and Estimate Accuracy Disclosure
- Given a max-segment threshold T is configured at account or community level, When the deduplicated total exceeds T, Then Send is disabled and a warning banner displays the current count and threshold. - Given the user has permission broadcast.override_max_segment, When Override and proceed is confirmed, Then Send is enabled and an audit log entry records the override with timestamp and user. - Given N ≤ 5,000 and exact calculation is used, Then the count is labeled Exact; Given N > 5,000 and estimation is used, Then the count is labeled Estimate with a ±% range and Last calculated timestamp. - Given estimation transitions to an exact count, When finalization completes, Then the displayed total updates without exceeding the stated ± range and the label switches to Exact. - Given a calculation or estimation error occurs, Then an error state is shown with retry options and Send remains disabled until a valid count is available.
Channel Simulation & Delivery Matrix
"As a community manager, I want to simulate which channels will deliver my message and why some recipients are unreachable so that I can adjust the plan and maximize read rates."
Description

Simulate delivery for each recipient based on channel preferences, verification status, opt-ins, and language settings to project how a broadcast will route (email, in-app, SMS, push). Present a delivery matrix with coverage percentages by channel and identify unreachable recipients with reasons. Support fallback rules (e.g., try push then email) and per-segment language rendering with translation previews when templates support localization. Integrate with Notification Service and User Preferences to ensure compliance with opt-out policies. Expected outcome: senders can anticipate deliverability, adjust content or channels, and prevent failed or noisy broadcasts.

Acceptance Criteria
Matrix Coverage by Channel for Filtered Segment
Given a broadcast with dynamic filters applied When the user runs Simulate Delivery Then the delivery matrix shows total recipients and per-channel routed counts (Email, In‑App, SMS, Push) based on resolved primary channel after applying fallback rules And each channel displays a percentage = routed count / total, rounded to one decimal place And channel percentages sum to 100% after rounding correction And the matrix refreshes within 2 seconds after any filter, preference, or fallback change And the matrix shows a Last updated timestamp in the user’s timezone
Unreachable Recipients and Reasons Panel
Given a simulated broadcast When simulation completes Then an Unreachable panel shows the total count of unreachable recipients And each unreachable recipient includes one or more specific reasons (e.g., email unverified, SMS opt‑out, no device token, channel disabled by preference, language not available and no default, account disabled) And the unreachable count equals the number of unique recipients with no allowable channels after fallback And when there are zero unreachable recipients, the panel displays 0 and no warnings
Fallback Routing Logic Simulation
Given fallback order "Push → Email → SMS → In‑App" is configured When the user runs Simulate Delivery Then for each recipient the first available, verified, and opted‑in channel in that order is selected as the resolved route And recipients missing a channel advance to the next fallback until one qualifies or none remain And the per-recipient resolved channel is used to compute the delivery matrix And the matrix displays the percentage of recipients routed via fallback (not first preference) And changing the fallback order updates results within 2 seconds
Opt-Out and Preference Compliance
Given recipients have per-channel opt-outs and organization-level suppressions When simulation runs Then no recipient is routed to any channel they have opted out of or that is suppressed by policy And if all channels are opted out/suppressed, the recipient is marked Unreachable with reason "All channels opted out/suppressed" And the simulation notes the User Preferences version/timestamp used for evaluation
Per-Language Rendering and Translation Preview
Given a localized template with supported languages and per-user language settings When the user runs Simulate Delivery Then the simulator groups recipients by language and shows counts and percentages per language And for each language a preview shows the rendered subject and first 200 characters using that locale And if a user’s language lacks a translation and default-language fallback is enabled, they are routed with the default locale and flagged as "default fallback" And if default fallback is disabled, the user is marked Unreachable with reason "No translation for preferred language" And updating template or language settings refreshes counts and previews within 2 seconds
Simulation Safety—No Live Sends
Given the user initiates Simulate Delivery When the simulation completes Then no notifications are dispatched to any channel And the UI labels the run as Simulation and disables send actions tied to the simulation results And an audit log entry is recorded with simulation parameters without creating delivery jobs
Notification Service and Preferences Integration Health & Staleness Handling
Given Notification Service and User Preferences APIs are available When simulation runs Then live verification and opt-in data are used to resolve routes And if either service is unavailable, a non-blocking warning is shown and cached data no older than 15 minutes is used, with the matrix labeled "stale" And simulation completes within 3 seconds for segments up to 10,000 recipients
Safeguards: Suppression, Frequency Caps & Quiet Hours
"As an admin, I want built-in suppression and send limits with quiet hours so that our broadcasts respect owner preferences and avoid overwhelming inboxes."
Description

Enforce broadcast safeguards including global and segment-level suppression lists, topic-based subscription honoring, per-user frequency caps, and community-aware quiet hours with time-zone handling. Provide pre-send checks with clear warnings (e.g., exceeds cap, violates quiet hours) and an override workflow for authorized roles. Include rate limiting to protect provider reputation and system stability. Offer a safe test mode that restricts delivery to seed/test accounts while preserving full metrics labeling. Expected outcome: reduced recipient fatigue, compliance with communication preferences, and lower risk of accidental spam.

Acceptance Criteria
Enforce Global and Segment-Level Suppression on Broadcast Send
Given a broadcast targets one or more communities and channels And one or more recipients exist on the global suppression list And one or more recipients match the broadcast’s segment-level suppression list When the sender previews recipients and initiates send Then all suppressed recipients are excluded from delivery on all channels And the live recipient count updates to reflect the exclusions before send And the preview explicitly lists excluded recipients with reason = Global Suppression or Segment Suppression and channel And an immutable audit log records each suppression exclusion with broadcastId, userId, channel, reason, timestamp And suppressed recipients cannot be re-included via override
Topic-Based Subscription Honoring Across Channels
Given recipients have per-channel topic subscription states (Subscribed, Unsubscribed, Digest) And the broadcast is tagged to specific topics and channels When the recipient set is computed and the pre-send check runs Then only recipients Subscribed (or matching Digest rules) for at least one tagged topic on a given channel are eligible on that channel And recipients Unsubscribed for all tagged topics on a channel are excluded for that channel And the pre-send summary displays counts excluded due to topic preferences by channel and topic And an authorized role may choose “Override topic preferences” per channel, with a required justification text And all preference-based exclusions and any overrides are recorded in the audit log with counts and justification And overridden deliveries include required compliance footer with a one-click manage-preferences link
Per-User Frequency Caps With Partial Send and Override
Given a configurable per-user frequency cap exists (default 3 broadcasts per 24 hours per channel across communities) And the system tracks per-user per-channel send history When a broadcast is prepared and pre-send checks execute Then any recipient whose inclusion would exceed the cap on any channel is excluded for that channel And the pre-send summary shows counts and identifies which recipients would exceed caps And the sender can proceed with a partial send to eligible recipients or schedule for the first time outside the cap window And an authorized role may override the frequency cap with a required justification; each override event is logged And cap enforcement is re-evaluated atomically at delivery time to prevent race conditions
Community Quiet Hours With Per-Recipient Time-Zone Enforcement
Given each community defines quiet hours per channel and a time zone And recipients inherit the community time zone unless an account-specific time zone is set When a broadcast is attempted that would deliver within a recipient’s quiet hours Then those recipients are excluded from immediate delivery and queued for delivery at the first allowed time And the pre-send summary shows counts by channel and community for quiet-hours exclusions and the next send time And the sender may choose partial send now (eligible only) or schedule the broadcast for the earliest allowed window And an authorized role may override quiet hours with a required justification; overrides are logged And delivery respects daylight saving transitions and computes time windows correctly for all recipients
Unified Pre-Send Checks, Warnings, and Authorized Override
Given a sender clicks Send on a prepared broadcast When the system runs validation checks (suppression lists, topic preferences, frequency caps, quiet hours, rate limits) Then a pre-send modal summarizes all issues with counts per rule and provides a downloadable CSV of affected recipients and reasons And the sender can choose to proceed with eligible recipients only, schedule, or request overrides for allowed rules And override options are restricted to authorized roles; global suppression is never overridable And any overrides require a justification text and are captured in the audit log with actor, role, timestamp, rules overridden, and counts And the final recipient list used for delivery is stored immutably with a reason code per exclusion
Safe Test Mode With Full Metrics Preservation
Given a sender enables Test Mode for a broadcast And seed/test accounts are designated at the community or organization level When the broadcast is sent in Test Mode Then delivery is restricted to designated seed/test accounts only across all channels And pre-send and preview screens show the test recipient list and counts clearly labeled as Test Mode And analytics record eligible pool, sent (test), excluded (Test Mode) counts, opens/clicks from test recipients, and tag the broadcast as Test And no non-test recipient receives a delivery even if an override is requested And all events generated in Test Mode are segregated in reporting filters
Channel and Provider Rate Limiting With Backoff
Given configurable per-channel and per-provider throughput limits exist (e.g., max N messages/second and M messages/minute) And the provider may signal throttling (e.g., HTTP 429 or rate-limit headers) When a broadcast exceeds the configured throughput Then the system throttles by queueing messages to respect limits without dropping deliveries And on provider throttling responses the system applies exponential backoff and retries within defined retry windows And real-time status displays indicate rate-limited sending and estimated completion time And delivery ordering is preserved per recipient, and no recipient exceeds caps due to retries And monitoring emits metrics for send rate, throttles, retries, failures, and SLA adherence
Scheduling, Drafts & Reusable Saved Segments
"As a part-time manager, I want to save segments and schedule drafts so that I can reuse targeting and send at optimal times without rebuilding lists."
Description

Enable saving and reusing named segments with ownership and sharing controls, and attach them to drafts that include message content, channel mix, and scheduling options (send now, later, or recurring windows). Validate segments at send time to account for dynamic membership changes and show delta since draft creation. Integrate with the existing announcements composer so managers can create a post and select a saved segment in one flow. Provide versioning for drafts and templates with change history. Expected outcome: managers work faster, avoid rebuilding audiences, and time messages for maximum impact without leaving Duesly.

Acceptance Criteria
Save Named Segment with Ownership and Sharing
Given I have defined filter criteria and have permission to create segments When I save the segment with a unique name and choose visibility (Private, Team, Organization) Then the segment is created with owner, visibility scope, timestamp, and live recipient count And the segment appears in search and pickers only to users allowed by its visibility scope And non-owners cannot edit or delete the segment but can clone it; owners and admins can edit/delete And saving with a duplicate name in the same visibility scope is blocked with a clear error and rename suggestion And an audit log entry records creation with name, filters, owner, and visibility
Use Saved Segment in Composer Without Leaving Flow
Given I open the announcements composer When I select a saved segment from the audience picker Then the recipient count updates in real time and matches the segment evaluation And I can preview who is included and excluded with reasons And I can compose content, select channel mix, and the draft auto-saves every ≤10 seconds or on field change And the draft retains the selected segment on reload and across sessions And attempting to schedule or send without a selected segment shows a blocking validation message
Draft Scheduling: Send Now, Send Later, and Recurring Windows
Given I have a draft with content, channel mix, and a selected segment When I choose Send Now and confirm Then validation passes for required fields and the send is queued immediately with the final recipient count displayed When I choose Send Later and set a date/time and timezone Then the time must be at least 2 minutes in the future, stores the chosen timezone, and shows the computed Next Run When I choose Recurring and define a rule (e.g., weekly Mon 9:00 community local time) Then the next three run times are displayed, I can pause/resume the schedule, and last-run is recorded after each execution And overlapping schedules for the same draft are prevented with an explanatory error
Segment Re-validation at Send Time with Delta Display
Given a draft attached to a saved segment was created at time T1 When I open the send confirmation at time T2 or the job fires Then the segment is re-evaluated and a delta summary shows recipients added and removed since T1 with counts And I can expand to view names and change reasons (e.g., dues status, role, tag change) And I can choose Proceed, Refresh Draft (update preview with current membership), or Cancel And if the resulting recipient count is 0, sending is blocked until I explicitly confirm override
Draft and Template Versioning with Change History
Given I edit a draft or template When I save changes Then a new immutable version is created with version number, timestamp, editor, and a diff of changed fields (content, channel mix, segment, schedule) And I can view version history and compare any two versions with highlighted differences And restoring a prior version creates a new head version without erasing history And the system records all version events in the audit log
Template Creation and Reuse from Drafts
Given I have a draft I want to reuse When I save it as a template with a name and visibility scope Then the template appears in the composer’s template picker for permitted users And creating from a template spawns a new draft prefilled with content and channel mix, with no send time by default And templates follow the same versioning and audit rules as drafts; editing a template creates a new version And deleting a template does not affect drafts already created from it
Ownership Transfer and Safe Deletion Rules for Segments and Drafts
Given I am the owner or an admin of a segment, draft, or template When I transfer ownership to another eligible user Then the new owner gains edit/delete rights and the transfer is captured in audit history When I attempt to delete a segment referenced by any active draft or schedule Then hard delete is blocked; I am offered to archive instead and shown impacted items And archived segments are hidden from pickers but remain linked for existing drafts And deleting a scheduled draft requires canceling its schedule before deletion, with a confirmation step
Audit Trail & Broadcast Analytics
"As a board secretary, I want a detailed audit and performance report for each broadcast so that I can document compliance and improve future communications."
Description

Capture an immutable audit trail of segment definitions, previews, approvals, and send events with timestamps, actor identity, and hashed recipient sets for privacy. Provide post-send analytics including deliveries, failures, read/open rates by channel, community, and segment, plus suppression and bounce reasons. Offer exportable reports and webhook callbacks for downstream compliance systems. Integrate with Duesly’s existing analytics pipeline and respect data retention policies. Expected outcome: transparent oversight, easier compliance reviews, and data-driven iteration to lift read rates and reduce follow-ups.

Acceptance Criteria
Immutable Audit Trail for Broadcast Lifecycle Events
- Given a manager defines/edits a segment, When they save the segment, Then an audit entry is appended within 60 seconds containing: timestamp (ISO8601 UTC), actor_id, actor_role, community_ids, broadcast_id, action_type=segment_saved, segment_filter_snapshot, recipient_count, recipient_set_hash, entry_checksum - Given a preview is generated, When the preview completes, Then an audit entry is appended with action_type=preview_generated and the same required fields (no raw recipient identifiers) - Given an approval is recorded, When approval is granted or rejected, Then an audit entry is appended with action_type=approval_granted or approval_rejected and approver_id - Given a send is initiated/completed, When each step occurs, Then audit entries action_type=send_initiated and action_type=send_completed are appended with counts and channel(s) - Given any attempt to mutate or delete an audit entry, When attempted, Then the system returns 403 and appends a tamper_attempt entry referencing entry_id - Given an API call to retrieve an audit trail for a broadcast, When executed, Then it returns chronologically ordered entries with verifiable SHA-256 entry_checksum and a chain_hash that validates order integrity
Deterministic Recipient Set Hashing Without PII Exposure
- Given a resolved recipient set for a broadcast, When computing the stored representation, Then only a deterministic SHA-256 hash of the sorted recipient_ids concatenated with a broadcast-specific salt is stored; no raw identifiers are persisted in the audit trail - Given the same input recipient set and broadcast salt, When hashed, Then the resulting recipient_set_hash is identical; if any recipient changes, Then the hash differs - Given preview visibility, When a user views preview recipients, Then visibility is governed by permissions; the audit trail continues to store only the hash - Given cross-broadcast comparison, When two broadcasts have identical recipient sets, Then their hashes differ due to distinct salts - Given API responses for audit entries, When inspected, Then no PII fields (emails, phone numbers, names, addresses) are present
Timely Post-Send Analytics by Channel, Community, and Segment
- Given a broadcast has completed sending, When up to 5 minutes elapse, Then analytics endpoints and UI expose per-channel metrics: delivered, failed, suppressed, bounced, unique_read/open count and rate, by community and by segment - Given analytics totals, When aggregating by channel/community/segment, Then subtotals equal the broadcast totals within the same time range - Given suppression and bounce events, When recorded, Then top reasons and provider codes are available with counts per channel - Given filters (time window, channel, community, segment), When applied, Then metrics reflect only the filtered scope and the query returns under 5 seconds for typical broadcasts - Given a resend to the same segment, When comparing analytics, Then metrics are partitioned by broadcast_id and not conflated
Exportable Reports and Webhook Delivery for Compliance
- Given a user requests an export for a broadcast or date range, When CSV or JSON is selected, Then the file contains audit metadata and analytics metrics with a documented schema and ISO8601 UTC timestamps - Given webhook subscriptions are configured, When events broadcast.sent and broadcast.analytics.updated occur, Then webhooks fire with an HMAC-SHA256 signature (X-Duesly-Signature) and an Idempotency-Key header - Given a webhook receiver returns non-2xx, When retries occur, Then exponential backoff with jitter is applied up to 24 hours; on success, delivery is marked and no further retries are attempted - Given duplicate deliveries, When the same Idempotency-Key is received, Then the consumer can safely dedupe; the platform does not reprocess analytics on duplicate acknowledgements - Given an export/webhook payload, When validated against the schema, Then required fields are present: broadcast_id, community_ids, segment_definition_id, channel, delivered, failed, suppressed, bounced, unique_reads, read_rate, reasons[]
Configurable Data Retention and Automated Purge
- Given an organization retention setting retention_days=N, When the nightly purge job runs, Then audit trail entries and detailed analytics older than N days are permanently deleted, and a data_purged audit entry is appended summarizing counts removed - Given legal hold is enabled for a broadcast or community, When the purge job runs, Then records under hold are retained until the hold is removed - Given a data export request for a period outside retention, When executed, Then the system returns 404 or an empty result with a message indicating data has been purged - Given PII minimization, When audit entries are inspected, Then only non-PII fields and recipient_set_hash are stored; no PII remains after purge - Given configuration change of retention_days, When updated, Then the new value is applied to subsequent purge cycles and logged in the audit trail
Integration with Duesly Analytics Pipeline and Observability
- Given broadcast lifecycle and delivery events are emitted, When they enter the analytics pipeline, Then they conform to the existing event schema version and pass validation with a 0% schema error rate in staging - Given at-least-once delivery to the pipeline, When duplicates occur, Then downstream de-duplication by event_id ensures dashboard counts are correct - Given pipeline health checks, When observed, Then metrics for lag, throughput, and error rate remain within configured SLOs and alerts trigger on breach - Given dashboards, When a broadcast completes, Then the existing analytics dashboards display the new metrics within the defined freshness window - Given a schema upgrade path, When a new field is added, Then versioning and backward compatibility are preserved without breaking existing consumers

Adaptive Clone

Replicate announcements and bills across communities while auto‑adapting amounts, due dates, GL codes, and fee policies to each community’s settings. Tokenized templates fill in names and contacts; per‑community diffs preview changes before launch. Cuts copy‑paste errors and speeds repeatable workflows.

Requirements

Multi-Community Clone Scope Selection
"As a board manager, I want to select multiple communities and content types for a clone so that I can distribute repeat communications and bills to the correct audiences in one action."
Description

Provide an interface to select target communities and content types (announcements, bills) for cloning, supporting filters by tags, cohorts, portfolio ownership, and community status. Enable bulk selection/deselection and saveable presets for recurring distributions. Honor role-based permissions to restrict cross-community actions and display eligibility indicators when a community lacks required configurations. Ensure seamless integration with the existing compose and feed workflows to reduce mis-targeting and streamline distribution.

Acceptance Criteria
Filtered Targeting by Tags, Cohorts, Portfolio, and Status
- Given no filters are applied and I have permission to view multiple communities When I open the Clone Scope selector Then I see all communities I am permitted to target - Given I select Tags [A,B], Cohort "2024 West", Portfolio "Smith PM", and Status "Active" When results refresh Then only communities that belong to the selected cohort, portfolio, and status AND have at least one of the selected tags are displayed - Given filters are applied When I change any filter value Then the result list and matching count update immediately to reflect the new filter - Given I toggle content types to Announcements and/or Bills When results refresh Then eligibility counts per community and the matching total reflect only those eligible for the selected content types - Given pagination or sorting is used When I navigate pages or change sort Then filters remain applied and counts remain accurate
Bulk Selection with Eligibility Awareness and Cross-Page Persistence
- Given filtered results include eligible and ineligible communities When I click Select All Then only eligible communities in the current result set across all pages are selected - Given M communities are eligible in the current result set When I click Select All Then the Selected counter displays M and the selection persists when I navigate between pages or adjust sort - Given I have used Select All and manually deselected some items When I click Deselect All Then the Selected counter returns to 0 and all manual selections are cleared - Given I change filters after selecting communities When results change Then selections still in the new result set remain selected, and selections outside the result set are retained and shown in the confirmation summary as "hidden by filters" - Given no eligible communities are selected When I attempt to Confirm Then the Confirm action is disabled with an explanatory message
Saveable Presets for Recurring Distributions
- Given I have configured filters, content types, and selection rules (e.g., Select All with exclusions) When I click Save as Preset and provide a unique name Then the preset is saved with the exact filter criteria, content types, and exclusion rules - Given a saved preset exists When I apply it Then the UI restores filters and content-type toggles, re-computes current eligibility against live data, reapplies Select All with prior exclusions, and updates counts - Given a preset is applied When a community is no longer eligible or no longer visible due to permissions Then it is excluded from selection and listed under "Excluded by policy" with reasons - Given I rename or delete a preset When I confirm the action Then the change persists and is visible across sessions - Given I attempt to save a duplicate preset name When I confirm Then I am prompted to overwrite or choose a different name; choosing overwrite replaces the existing preset
Role-Based Permission Enforcement Across Communities
- Given my role lacks cross-community clone permission When I open the selector Then communities outside my assigned portfolio(s) are not shown and bulk actions are limited to permitted communities - Given I have read access but not clone permission for some visible communities When the list renders Then their selection checkboxes are disabled with a tooltip explaining "Insufficient permission" - Given my selection (via stale state or preset) includes disallowed communities When I click Confirm Then the action is blocked, disallowed items are removed from selection, and a banner lists which items were removed and why - Given a clone is successfully initiated When audit logging occurs Then the record includes my user/role, timestamp, target community IDs, and any permission-based exclusions
Eligibility Indicators for Required Configurations by Content Type
- Given content type Bills is selected When a community lacks required billing configurations (e.g., GL codes, payment processor, fee policy) Then the community is marked Ineligible with an icon and tooltip listing specific missing items, and its checkbox is disabled - Given content type Announcements is selected When a community requires no special configuration Then no ineligibility indicator is shown and the community remains selectable - Given multiple content types are selected When a community is eligible for Announcements but not Bills Then the UI shows Partial eligibility and the confirmation summary discloses that only Announcements will be targeted for that community - Given I click View reasons in the eligibility panel When the modal opens Then it lists counts by reason and provides links to each community’s settings, respecting my permissions
Compose and Feed Workflow Integration with Confirmation and Summary
- Given I am composing an Announcement or Bill When I click Select Communities Then the scope selector opens without losing any draft content - Given I confirm a selection When I return to compose Then the draft shows a distribution summary with total selected, partial eligibilities, and excluded counts with reasons, plus links to preview per-community diffs where applicable - Given I publish the item When I view the feed Then a distribution summary card shows targeted, delivered, and excluded counts with a link to an audit trail of scope selection and eligibility reasons - Given I cancel the selector without confirming When I return to compose Then no changes are made to the existing target selection
Auto-Adaptation Rules Engine
"As a treasurer, I want cloned bills to automatically conform to each community’s amounts, codes, and fee rules so that I don’t have to manually edit dozens of variations."
Description

Implement a deterministic rules engine that auto-adjusts amounts, due dates, GL codes, and fee policies per community based on stored settings, policy hierarchies, and per-community overrides. Support formulas (percent, fixed, proration), min/max caps, weekend/holiday roll-forward, grace periods, and time zone normalization. Provide an admin console for testing rules, versioning, and rollback, with safe defaults when data is missing. Integrate with community financial settings and accounting exports to keep cloned bills compliant and accurate without manual edits.

Acceptance Criteria
Amount Adaptation: Formulas, Proration, and Caps
Given base_amount=100.00 and rules percent_surcharge=10%, proration=50%, min_cap=70.00, max_cap=120.00 When the rules engine adapts the bill Then final_amount=70.00 after applying percentage, proration, rounding(2 decimals), and caps in that order Given base_amount=200.00 and rules percent_surcharge=0%, proration=100%, min_cap=0.00, max_cap=180.00 When the rules engine adapts the bill Then final_amount=180.00 due to max_cap enforcement Given proration input is missing When the rules engine adapts the bill Then proration defaults to 100% and is recorded in the rule trace
Due Dates: Weekend/Holiday Roll-Forward, Grace Periods, and Time Zone Normalization
Given a template due_at that falls on a weekend in the community time zone and roll_forward_policy=next_business_day and due_time_policy=end_of_day_local When the rules engine adapts the due date Then due_at is moved to the next business day at 23:59:59 in the community time zone Given a template due_at that falls on a community holiday listed in the community holiday calendar When the rules engine adapts the due date Then due_at is moved to the next non-holiday weekday in the community time zone Given grace_period_days=5 When calculating late_fee_start_at Then late_fee_start_at = adapted due_at + 5 calendar days at 00:00:00 in the community time zone Given inputs contain mixed time zones When adaptation completes Then all stored timestamps are normalized and persisted in the community time zone with UTC offsets recorded in the audit trail
GL Code Mapping: Hierarchy and Overrides
Given global_default_gl=4000, community_default_gl=4100, and an override gl for charge_type="Assessment" of 4200 When adapting a bill with charge_type="Assessment" Then gl_code=4200 and the rule trace shows override > community_default > global_default precedence Given no override exists but community_default_gl exists When adapting a bill Then gl_code=community_default_gl and the rule trace documents the selection Given neither override nor community_default_gl exist but global_default_gl exists When adapting a bill Then gl_code=global_default_gl and a warning is not issued Given no mapping exists at any level When adapting a bill Then gl_code="UNASSIGNED" and a warning is logged with severity=warning and remediation guidance
Fee Policies: Late and Convenience Fees Application
Given late_fee policy flat_amount=25.00 and grace_period_days=5 When payment is posted after adapted due_at + 5 days Then a $25.00 late fee is applied once and recorded on the ledger Given percentage_late_fee=1.5% per month with max_cap=100.00 When late fee is computed Then fee_amount = min(1.5% of outstanding_principal, 100.00) rounded to 2 decimals Given convenience_fee_enabled=false at the community level When adapting a bill for online payment Then no convenience fee is added Given fee policies are missing When adapting a bill Then no fees are applied and a warning is logged; safe defaults are reflected in the rule trace
Admin Console: Test Harness, Versioning, and Rollback
Given an admin selects community X, rule_set_version=v2, and provides test inputs When Test Rules is executed Then the console displays a before/after preview for amount, due_at, gl_code, and fees with a deterministic rule evaluation trace and no data is persisted Given a change to rules is published When saved Then a new immutable rule_set_version is created with audit log (actor, timestamp, diff) and the previous version remains available Given an admin rolls back to rule_set_version=v1 When adaptation runs for community X with the same inputs Then outputs exactly match those produced by v1 (byte-for-byte where applicable) Given the same inputs and the same rule_set_version When tests are run multiple times Then outputs are identical (deterministic) and execution time is within the configured SLA threshold
Safe Defaults and Failure Modes
Given the community time_zone is missing When adapting timestamps Then the engine defaults to the organization time_zone; if unavailable, UTC is used and a warning is logged Given the community holiday calendar is missing When adapting due dates Then only weekend roll-forward is applied and the rule trace notes the missing calendar Given required base_amount is missing or invalid When adapting a bill Then the bill is marked status="Needs Attention", no publication occurs, and an error is logged with a correlation_id Given partial fee policy inputs (e.g., missing cap) When computing fees Then safe defaults per spec are applied and warnings are logged without blocking publication
Accounting Exports Integration and Compliance
Given an adapted bill with gl_code, amount, due_at, and identifiers When generating the configured accounting export Then the record conforms to the export schema, passes validation, and uses the community's chart-of-accounts mapping Given multiple communities are included in a batch export When generating the export Then records are segmented per community as configured and totals per community equal the sum of included line items Given re-export prevention is enabled When attempting to export a previously exported batch Then the system blocks the export or marks it as a re-export with a new batch_id according to policy and logs an audit entry Given a dry-run export is triggered from the admin console When executed Then no external systems are modified and a downloadable sample file is produced for review
Tokenized Templates and Directory Merge
"As a community manager, I want to use tokenized templates that pull correct names and contacts so that messages feel personalized without manual edits."
Description

Create a templating system with tokens for community name, board/manager contacts, owner/resident segments, property addresses, and custom fields. Pull values from the Duesly directory and community-level attributes, supporting fallbacks, conditional blocks, and localization. Validate tokens at compose and clone time to prevent unresolved placeholders, and provide a token inspector to preview resolved values. Ensure compatibility with announcements and bills so content remains personalized while remaining scalable across communities.

Acceptance Criteria
Compose: Block publish on unresolved tokens
Given I am composing an announcement or bill in the editor with a selected community context When I click Preview or Publish Then the system validates all tokens and conditional blocks against the selected community and the platform token registry And highlights each invalid, unknown, or unresolved token inline with line/column and message And prevents Publish if any token errors remain; Publish button is disabled with an error summary listing token names and counts And allows Publish only when 0 token errors and 0 unresolved placeholders are detected And validation completes within 300 ms for templates up to 5,000 characters; 95th percentile <= 500 ms
Clone: Cross-community token resolution and adaptation
Given I clone a tokenized template to multiple target communities (up to 200) When I open the clone dialog and select communities Then pre-validation runs for each target community and applies locale-specific formatting (dates, numbers, currency) per community settings And any community with unresolved/unknown tokens is flagged "Requires Fix" with per-token details; those communities are excluded by default from launch And I can proceed to launch for all passing communities while leaving failing ones unselected And a summary shows total communities scanned, pass/fail counts, and max/avg validation time And end-to-end validation for 200 communities completes within 5 seconds (p95 <= 7 seconds)
Token Inspector: Per-community and per-recipient preview
Given I open the Token Inspector from the editor When I select a community and choose a sample recipient (by name/email/ID) or a segment Then the inspector shows Raw vs Resolved views with all tokens resolved, including evaluation of conditional blocks And missing data uses defined fallbacks; if no fallback exists the token is marked as an error And I can page through at least 50 sample recipients without leaving the inspector And first load renders within 500 ms; subsequent recipient switches render within 200 ms (p95 <= 350 ms)
Fallbacks and conditional logic resolution
Rule: Tokens support default fallbacks (e.g., {{contact.phone | default:"(none)"}}); when the source value is null/empty, the fallback is inserted Rule: Conditional blocks support if/elseif/else with existence and equality operators; nested blocks up to depth 3 are supported Rule: Unknown variables or malformed conditions produce validation errors with line/column and do not render Rule: If a token lacks data and no fallback is provided, Publish/Launch is blocked until resolved Rule: Final rendered content must contain 0 unresolved placeholders matching pattern {{...}}
Localization: Dates, amounts, and numbers by community locale
Given a community has a locale and currency configured When rendering tokens for dates, numbers, and amounts Then formatting follows the community locale (CLDR-compliant) for decimal/grouping separators, date order, and currency symbol placement And currency amounts display with two decimal places where applicable; no conversion of the numeric value occurs; rendered text matches expected fixtures for test cases And localized formatting is consistent in the editor preview, Token Inspector, and clone previews across en-US, es-MX, and fr-CA test locales And if a locale is unsupported, the system falls back to en-US and displays a non-blocking warning
Compatibility: Announcements and Bills rendering
Rule: Shared tokens (community_name, recipient_name, property.address, contacts.*, custom.*) resolve in both Announcements and Bills Rule: Billing-only tokens (amount, due_date, gl_code, fee_policy) are allowed only in Bills; using them in Announcements yields validation errors listing each offending token Rule: In Bills, billing tokens resolve from bill payload and community policy; missing required billing token values block Publish with specific messages Rule: Rendering output matches Preview byte-for-byte for both HTML and plaintext modes
Security: Community data isolation in token resolution
Rule: Token resolution is scoped to the selected community; data from one community never appears in another community’s preview, inspector, or launch output during cloning Rule: Access controls apply; tokens only resolve fields the current user is authorized to view; unauthorized tokens return validation errors and are not rendered Rule: Audit logs record token rendering events with user, template ID, community ID, timestamp, and counts of resolved tokens; PII values are masked in logs Test: Clone a template to 50 communities with distinct contacts; verify 0 cross-community data leaks and all audit entries are present
Per-Community Diff Preview
"As a reviewer, I want to see exactly what will change for each community before sending so that I can catch errors and approve with confidence."
Description

Offer a preview interface showing side-by-side diffs between the source item and each target community’s adapted version, highlighting changes to amounts, due dates, GL codes, fee policies, and resolved tokens. Allow filtering to show only differences, bulk accept/override of fields, and inline editing for exceptional cases. Provide pagination for large portfolios and exportable diff reports for offline review. Integrate approvals with role-based permissions to ensure proper sign-off before launch.

Acceptance Criteria
Render Side-by-Side Diff With Token Resolution
Given a source item and two or more target communities with distinct settings When the user opens the Per-Community Diff Preview for the selected targets Then a side-by-side view of Source vs Adapted is rendered for each target community And changes in Amount, Due Date, GL Code, Fee Policy, and tokenized text are highlighted at field level And 100% of tokens are resolved using the target community’s data with no placeholders remaining And any unresolved token is flagged inline as an error and the affected community cannot be approved
Differences-Only Filter and Change Counts
Given the Per-Community Diff Preview is visible When the user enables Show only differences Then only fields with changes are displayed for each community And each community tile shows a badge with the number of changed fields And communities with no differences display a No differences state And toggling the filter off restores the full field list And applying or removing the filter completes within 300 ms at p95 for up to 50 communities on the page
Bulk Accept/Override of Fields Across Communities
Given the user has selected one or more fields and one or more target communities When the user chooses Bulk accept adapted values Then the selected adapted values are marked as accepted for all selected communities and pending changes are cleared And the action is undoable within the current session Given the user has selected one or more fields and one or more target communities When the user chooses Bulk override and enters a value Then the override value is applied to all selected communities where it passes validation and failures are reported inline without applying invalid changes And all bulk actions are captured in the audit log with user, timestamp, scope, and before/after values
Inline Editing With Validation and Audit Trail
Given the user clicks an editable field in a community’s diff When the field enters edit mode Then the appropriate input control is shown with constraints (e.g., currency format, date picker, GL code selector) And on save, client- and server-side validations run and success updates the adapted value and highlights it as overridden And validation failures display inline error messages without saving changes And the edit is recorded in the audit log with user, timestamp, and before/after values
Pagination and Selection Persistence at Scale
Given a portfolio containing up to 10,000 target communities When the Per-Community Diff Preview is loaded Then results are paginated with a default page size of 50 and options for 50/100/250 per page And initial page render completes within 2.0 seconds at p95 and page-to-page navigation within 1.0 second at p95 And filters, the differences-only toggle, and column visibility persist across page navigation And community and field selections persist across pages for bulk actions and can be cleared with a single action
Exportable Diff Report (CSV/PDF) Respecting Filters
Given the current diff preview has filters, selections, and the differences-only toggle applied When the user exports as CSV or PDF Then the exported file includes one row per community with columns for community ID/name, change flags, before/after values for Amount, Due Date, GL Code, Fee Policy, token resolution status, override flag, approval status, and last updated timestamp And the export reflects the current filters and the differences-only setting And CSV uses UTF-8 with headers; PDF paginates legibly for standard A4/Letter And exports up to 10,000 communities complete within 10 seconds at p95 and are available for download with a unique URL And the export action is logged with user, timestamp, and parameters
Role-Based Approvals and Launch Gate
Given role-based permissions are configured for Preparer and Approver roles When a Preparer submits selected communities for approval Then only users with the Approver role can approve or reject per community or in bulk And approval is blocked for any community with validation errors or unresolved tokens And upon approval, the community’s adapted item is locked from further edits except by users with an Override permission And launch of the Adaptive Clone job is blocked until all required approvals are recorded for the selected communities And all approval and rejection actions trigger notifications and are written to the audit log
Preflight Validation and Compliance Checks
"As a compliance-focused board member, I want preflight checks to flag issues before launch so that we avoid bad bills or noncompliant announcements."
Description

Perform automated preflight checks to detect issues such as past due dates, missing or inactive GL codes, misconfigured fee policies, token resolution failures, duplicated invoice numbers, audience mismatches, and permission violations. Present actionable errors with one-click fixes where possible, block launch on critical failures, and allow warnings with explicit acknowledgment. Generate a validation report attached to the item’s audit record to improve accountability and reduce copy-paste errors at scale.

Acceptance Criteria
Block Launch on Past‑Due Dates
Given a cloned bill targeting one or more communities And any target community has a due date earlier than its current local date When the user runs Preflight or attempts to Launch Then the system flags a Critical error per affected community labeled "Past due date" And the Launch action is disabled And a one‑click fix "Adjust to next business day" is offered using the community’s timezone and calendar rules And after applying the fix, the due date updates in the per‑community preview and the Critical error is cleared And Launch is re‑enabled only when no Critical errors remain
GL Code Validation and Auto‑Mapping
Given a cloned bill with one or more line items When Preflight runs Then any missing GL code or GL code inactive in a target community is flagged as Critical per community and line And a one‑click fix "Map to community default revenue GL" is available where a default exists And applying the fix updates the line item preview and records the mapping in the validation report And if no valid mapping exists, the item remains Critical and Launch stays disabled
Duplicate Invoice Number Enforcement
Given invoice numbers are generated or provided for the cloned bills When Preflight runs across all target communities Then any invoice number that is not unique within its community and fiscal year is flagged as Critical And a one‑click fix "Regenerate using community numbering scheme" is available And applying the fix produces unique numbers and updates the per‑community preview And Launch is blocked until all duplicates are resolved
Token Resolution Integrity
Given the item uses tokenized templates (title, body, line item memo, subject) When Preflight runs Then all tokens must resolve to concrete values for each target community and recipient context And any unresolved required token is flagged as Critical with the missing source field identified And unresolved optional tokens are flagged as Warnings with default substitutions shown And a quick action to open the editor at the offending field is provided And Launch is blocked if any Critical token failures remain And Warning cases require explicit acknowledgment before Launch
Fee Policy Consistency and Compliance
Given late fee and processing fee policies may vary by community When Preflight runs Then any referenced policy that is disabled, conflicting, or exceeds the community cap is flagged (Critical if invalid/disabled, Warning if nearing or requiring acknowledgment) And the preview displays the computed fee outcome per community And a one‑click fix allows selecting a valid active policy or disabling fees for this item And Launch is blocked until all Critical fee policy issues are resolved And any remaining Warnings require explicit acknowledgment to proceed
Audience Validity and Permission Enforcement
Given the user selects an audience segment for the cloned item When Preflight runs Then segments that resolve to zero recipients per community are flagged as Critical with the community listed And any recipients outside the user’s permission scope are flagged as Critical with counts per community And a quick action "Adjust audience" is available to modify filters to an allowed set And recipients with paper‑only or do‑not‑email preferences generate Warnings with counts and export link for print workflows And Launch is blocked until all Critical audience and permission issues are resolved And Warnings require explicit acknowledgment to proceed
Validation Report Generation and Audit Attachment
Given Preflight completes with Pass, Warning, or Fail outcomes When the results are finalized Then the system generates a validation report containing: timestamp, user ID, item ID, version, target community list, counts by severity, detailed issues, applied auto‑fixes, and any acknowledgments And the report is attached to the item’s immutable audit record and is downloadable and viewable And launches that proceed with Warnings record who acknowledged, what was acknowledged, and when And if no issues are found, the report still records a Pass with zero issues And attempting Launch without a current report (older than the last edit) triggers an automatic Preflight and report regeneration
One-Click Convert and Clone from Feed
"As a manager, I want to convert an announcement into a cloned bill across communities with one click so that I can bill quickly without rebuilding content."
Description

Enable a single action from any feed item to convert an announcement into a bill (or duplicate an existing bill) and immediately initiate the Adaptive Clone flow with context pre-filled. Carry over attachments, tags, audiences, and reminders; auto-detect item type; and present scope selection and per-community diffs inline as a modal. Reduce context switching by integrating with the existing feed UI and keyboard shortcuts, accelerating repeatable billing and announcement workflows.

Acceptance Criteria
One-Click Convert & Clone from Feed Item
Given a user with permission to create bills is viewing a feed item that is an Announcement, When the user activates Convert & Clone from the item’s action menu, Then a Bill draft is created from the announcement, the Adaptive Clone modal opens inline over the feed, and the page does not navigate. Given a user with permission to create bills is viewing a feed item that is a Bill, When the user activates Convert & Clone, Then a duplicate Bill draft is created, the Adaptive Clone modal opens inline, and the page does not navigate. Given a user without required permissions views a feed item, When the user attempts to activate Convert & Clone, Then the action is disabled or shows an “Insufficient permissions” message and no draft is created or opened.
Keyboard Shortcut Triggers Convert & Clone
Given focus is on a feed item (via mouse hover or keyboard selection) and the user has permission, When the user presses Ctrl/Cmd+Shift+C, Then the Convert & Clone flow is invoked for the focused item with the same outcome as clicking the action. Given no feed item is focused, When the user presses Ctrl/Cmd+Shift+C, Then no action is taken and a non-intrusive hint indicates how to focus an item. Given a modal or text input has focus, When the user presses Ctrl/Cmd+Shift+C, Then the shortcut is not intercepted by the page and no Convert & Clone is triggered.
Adaptive Clone Modal Pre-Fills Context
Given Convert & Clone is initiated from a source item, When the Adaptive Clone modal opens, Then it pre-fills title/subject, body/details, attachments, tags, audience, and reminders from the source. Given the source item is a Bill, When the modal opens, Then amount, due date, GL code, and fee policy are pre-filled from the source bill. Given the source item is an Announcement, When the modal opens, Then bill fields (amount, due date, GL code, fee policy) are initialized from the current community’s defaults and mapping rules, and title/body are mapped into bill subject/description. Given the modal is open, When the user has not yet confirmed, Then no changes are persisted to any community drafts.
Per-Community Scope Selection and Diffs
Given the modal is open, When the user opens Scope selection, Then only communities where the user has create-billing permission are selectable and the user can multi-select, search, and Select All. Given communities are selected, When diffs are computed, Then each community row displays highlighted diffs for Amount, Due Date, GL Code, and Fee Policy, plus tokenized fields (names/contacts), relative to the source. Given a community is selected, When the user overrides a field in that community’s row, Then the diff updates immediately and the override is included in the launch payload. Given diffs are available, When the user toggles Show only changes, Then communities without changes are hidden from the preview.
Carry Over Attachments, Tags, Audiences, and Reminders
Given the source item has N attachments, When Convert & Clone is initiated, Then the modal shows N attachments ready to copy and the same files appear on the created items after launch. Given the source item has tags and an audience selection, When Convert & Clone is initiated, Then the same tags and mapped audiences are pre-selected; where a target community uses different audience IDs, a correct ID mapping is applied. Given the source item has reminder rules, When the Adaptive Clone modal pre-fills, Then reminders are copied and auto-adjust relative to each community’s due date and fee policy. Given an attachment type is unsupported in any target community, When the user reviews the modal, Then an inline warning identifies the impacted communities and offers Skip or Replace for that file without blocking other communities.
Error Handling, Partial Success, and Audit Logging
Given one or more selected communities lack required configuration (e.g., GL code or fee policy), When the user clicks Launch, Then valid communities proceed and invalid ones are listed with inline resolution controls to set missing values and retry. Given Launch completes, When results are available, Then a summary shows counts of successes and failures and per-community errors are visible and exportable. Given a Convert & Clone is launched, When auditing the action, Then an audit log entry records initiator, timestamp, source item ID and link, selected communities, per-community amount/due date/GL/fee policy values, reminder schedules, and result status per community.
Performance, Progress, and Accessibility of Inline Modal
Given the feed page is loaded, When the user invokes Convert & Clone, Then the Adaptive Clone modal renders within 500 ms (P50) and 1000 ms (P95) on supported desktop environments. Given the modal opens, When context pre-fill occurs, Then required data (source fields and community settings) appears within 1 second and a skeleton/loader is shown until complete. Given 50 communities are selected, When diffs are generated, Then initial diffs display within 3 seconds with progressive rendering and a progress indicator; the UI remains responsive. Given the modal is visible, When navigating via keyboard, Then focus is trapped inside the modal, all actionable controls are reachable, Escape closes the modal, and ARIA roles/labels are present for screen readers.
Audit Trail and Traceability
"As an auditor or board secretary, I want a complete log of cloning actions and outcomes so that we can demonstrate due process and trace any mistakes."
Description

Record an immutable audit trail for each clone operation, including initiator, timestamp, source item, target communities, rules engine version, preflight results, per-community diffs, overrides, approvals, and final outcomes. Surface logs in a searchable UI and provide export options for auditors. Snapshot configuration and rule versions to ensure reproducibility of results and support investigations into discrepancies or resident disputes.

Acceptance Criteria
Immutable audit record per clone with complete fields
Given a user initiates an Adaptive Clone and confirms execution When the clone operation starts Then the system assigns a unique operation_id and records initiator_id, initiator_role, and started_at in UTC ISO-8601. Given the clone operation completes (success or failure) When the audit record is finalized Then it includes: source_item_id/type/version, target_community_ids, rules_engine_version_id, ruleset_checksum, environment, preflight_results_by_community, diffs_by_community, overrides_with_actor/timestamps/justifications, approvals_with_actor/role/timestamps/method, outcomes_by_community (success|fail, error_code, error_message), and completed_at. Given an audit record is written When stored Then it includes content_hash and previous_record_hash to form a tamper-evident chain. Given API or UI access to audit records When retrieving an audit record by operation_id Then the full JSON payload is returned without loss or truncation up to 2 MB per operation with server-side pagination for larger payloads.
Snapshot rules and configuration for reproducibility
Given a clone is executed When the rules engine evaluates inputs Then the audit record stores an immutable snapshot: rules_engine_version_id, rules_definition_json, community_policy_snapshot per target community, input_data_snapshot (token-resolved values), and external rate tables with checksums. Given a snapshot exists for operation_id When a dry-run re-execution is performed in a sandbox using the snapshot Then preflight_results and diffs match exactly; any mismatch triggers a non_reproducible flag on the audit record. Given a snapshot exceeds 5 MB When stored Then large artifacts are offloaded to object storage and referenced via time-limited signed URLs in the audit record.
Searchable audit log UI with filters and performance SLAs
Given an authorized user opens Audit Logs When filtering by date range, initiator, community, source item, outcome, rules version, has_overrides, or has_approvals Then results reflect filters accurately and counts update accordingly. Given a dataset of up to 50,000 audit records When applying any single filter or a combination of up to three filters Then p95 response time is ≤ 2 seconds and p99 is ≤ 4 seconds. Given the results grid When sorting by started_at or initiator and paginating (25/50/100 per page) Then sorting is correct and stable across pages and the total count remains accurate. Given a keyword search box When searching by operation_id, community name, user email, or error code Then matching records are returned with relevant fields highlighted.
Export logs for auditors (CSV/JSON) with integrity
Given an auditor applies filters in the Audit Logs UI When exporting as CSV or JSON Then the export contains only filtered records, includes all defined fields, and preserves data types in JSON. Given an export exceeds 20,000 records or 50 MB When requested Then it runs as a background job, notifies on completion, provides a time-limited signed download URL, and chunks files into parts ≤ 50 MB with an index manifest. Given an export is generated When downloaded Then each file includes a SHA-256 checksum and the manifest includes a cumulative checksum; the export event (requesting_user, timestamp, filters) is logged in the audit trail. Given PII redaction policies When exporting Then masked fields in UI are masked in exports unless the user has PII_Unmask permission; permission checks are enforced and logged.
Immutability enforcement and tamper detection
Given any user, including admins When attempting to edit or delete an audit record via UI or API Then the system returns 403 Forbidden and logs the attempt with actor, endpoint, and reason. Given append-only storage is configured When a new audit record is added Then it is written to write-once storage, stamped with content_hash and previous_record_hash, and replicated to a separate region. Given a daily integrity check job When it runs Then it verifies the hash chain for all records created in the last 365 days and raises a Sev-1 alert if any break is detected.
Visibility of preflight, diffs, overrides, and approvals
Given an audit record detail view When opened Then it renders preflight results, per-community diffs, overrides, and approvals with timestamps and actors, and supports download of the exact preflight report. Given an override is entered When saving Then a justification text of at least 15 characters is required and captured; the override appears with before/after values and context in the audit record. Given an approval workflow is required When approvals are submitted Then the audit record stores approver identity (IDP subject), method (SAML/OAuth), timestamp, and a non-editable signature; the clone cannot execute until required approvals are present.
End-to-end traceability for disputes
Given a resident dispute references an invoice_id or resident_id When support searches Audit Logs by those identifiers Then the system returns the originating clone operation and the rule and inputs that set the disputed amount. Given the trace is opened When viewing rule evaluation details Then the audit displays the exact input variables, resolved token values, and the calculation path that produced the amount. Given a shareable audit bundle is requested When generated Then the system creates a redacted PDF/JSON bundle with a 7-day expiration, signed URL, and event logged; access to the bundle is permission-gated and audited.

KPI Heatmap

See read rates, payment completion, delinquency shifts, and reminder effectiveness at a glance across your portfolio. Color‑coded tiles and trend lines highlight outliers; click to drill into segments or export. Set thresholds to surface at‑risk communities so you can act before issues spread.

Requirements

KPI Computation Engine
"As a portfolio manager, I want KPIs computed consistently across all communities so that I can trust the heatmap and make apples-to-apples comparisons."
Description

Centralized service that defines and computes portfolio- and community-level KPIs—announcement read rate, payment completion, delinquency shift, and reminder effectiveness—on configurable time windows (e.g., 7/30/90 days). Ingests events from Duesly announcements, billing/payments, and reminder systems; persists normalized, time-series aggregates for fast retrieval and trend rendering. Supports backfill and scheduled recomputes, idempotency for late-arriving events, versioned formula definitions, and data quality checks. Exposes a read-optimized API used by the heatmap, trend lines, and exports. Multi-tenant by portfolio, with row-level scoping and caching to meet performance SLAs.

Acceptance Criteria
Event Ingestion and Normalization Across Duesly Systems
- Given events from announcements, billing/payments, and reminders include tenant, portfolio, community, user, and occurred_at, When ingested, Then they are normalized into a canonical schema and stored within 60 seconds of receipt. - Given duplicate or retried events share an idempotency key or composite unique key, When processed multiple times, Then only one normalized record is persisted and downstream aggregates are not double-counted. - Given an event with an unknown schema version or invalid fields, When encountered, Then it is quarantined with a rejection reason, excluded from aggregation, and an alert is emitted within 5 minutes. - Given multi-tenant inputs, When ingesting, Then writes are scoped to the tenant/portfolio and audit metadata (source, version, processed_at) is recorded.
Compute Announcement Read Rate Over Configurable Windows
- Given announcements and read events exist, When the compute job runs, Then read rate per community and portfolio is calculated for 7/30/90-day trailing windows (tenant timezone) as unique_readers / unique_targeted_recipients. - Given multiple reads by the same user for the same announcement, When computing, Then only one read per user-announcement counts toward the numerator. - Given the computation completes, When persisting, Then aggregates include keys: tenant_id, portfolio_id, community_id (nullable for portfolio-level), kpi="announcement_read_rate", window, value, numerator, denominator, formula_version, as_of_date, generated_at. - Given recomputation or replays, When executed, Then results are idempotent with upsert semantics and do not create duplicate time-series points.
Compute Payment Completion KPI With Late-Arriving Events and Idempotency
- Given invoices issued and payment events, When the compute job runs, Then payment completion rate is calculated for 7/30/90-day trailing windows as paid_invoices / issued_invoices where paid_invoices include invoices issued in the window that are paid by as_of_date. - Given a late-arriving payment event for an invoice in scope, When received before the next scheduled recompute, Then the next recompute updates the affected aggregates within 60 minutes without double counting. - Given partial payments, When computing paid_invoices, Then an invoice counts as paid only when total_paid >= amount_due (tolerance <= $0.01). - Given results are persisted, When queried later, Then the stored aggregates reflect the last processed event watermark for transparency (watermark_timestamp stored).
Compute Delinquency Shift KPI and Persist Trends
- Given account balances and due statuses snapshot daily, When computing, Then delinquency shift is produced per day per community and portfolio with fields: count_delta, amount_delta, and rate_delta (% households delinquent) comparing current_day vs prior_day. - Given backfilled historical snapshots, When recomputing for a date range, Then delinquency shift values match deterministic recalculation and are consistent across reruns (no drift). - Given households transition in and out of delinquency multiple times in the window, When computing count_delta, Then changes reflect net movement between consecutive days (entries minus exits). - Given aggregates are persisted, When rendering trends, Then time-series points are available at daily granularity with formula_version and as_of_date keys.
Reminder Effectiveness KPI Using Pre/Post Windows
- Given reminder events tied to announcements or invoices, When computing, Then effectiveness is calculated as uplift = (conversion_rate_post - conversion_rate_pre) where conversion is read or paid within X days (configurable, default 7) after reminder vs the same length window before. - Given multiple reminders for the same target, When computing, Then only the first reminder within the lookback contributes to that target to avoid double attribution. - Given segmentation by community and portfolio, When persisting, Then aggregates store window_days, baseline_rate, post_rate, absolute_uplift_pp, relative_uplift_pct, sample_size, and formula_version. - Given sparse data (sample_size < 30), When computing, Then effectiveness is still stored but flagged low_sample=true for consumer UI to handle.
Versioned Formula Definitions, Backfill, and Scheduled Recompute
- Given formula definitions are stored with semantic versions (major.minor.patch), When a new version is activated, Then new aggregates are tagged with the new formula_version while prior data remains queryable by version. - Given a backfill request with tenant/portfolio scope and date range, When executed, Then the job processes deterministically with checkpointing, retries on failure, and idempotent upserts, producing no duplicate aggregates. - Given the scheduler is enabled, When operating, Then recompute runs at least hourly per tenant and responds to late-arriving events and version changes within 60 minutes. - Given a formula version is rolled back, When recomputing, Then aggregates are regenerated for the affected date range with the selected prior version and audit logs reflect the change (who, when, from_version, to_version).
Read-Optimized KPI API With Multi-Tenant Scoping, Caching, and Performance SLA
- Given a portfolio-scoped access token, When calling GET /kpis/heatmap?portfolio_id=...&windows=7,30,90, Then only communities in that portfolio are returned with KPIs and no data from other tenants (row-level isolation enforced). - Given typical heatmap queries (<=200 communities, 4 KPIs, 3 windows, last 30 days), When served from warm cache, Then p95 latency <= 400 ms and p99 <= 800 ms; When cold, Then p95 <= 800 ms. - Given trend requests, When calling GET /kpis/trends?scope=community_id&kpi=...&from=...&to=..., Then a contiguous daily time series including value, numerator, denominator, as_of_date, and formula_version is returned. - Given export requests, When calling GET /kpis/export?portfolio_id=...&from=...&to=..., Then a streamed CSV is produced with schema documented and completes for 12 months of daily data within 60 seconds for <=200 communities. - Given caching is enabled, When responses are generated, Then they include ETag and Cache-Control headers; subsequent conditional requests with If-None-Match return 304 when unchanged; cache keys include tenant, scope, windows, and formula_version.
Heatmap Visualization & Color Scales
"As a board member, I want a color-coded heatmap that highlights outliers so that I can spot issues at a glance without digging through reports."
Description

Responsive heatmap grid that displays KPIs per community using accessible color scales mapped to threshold states (OK/Watch/At-Risk). Provides legends, tooltips with exact values and last-updated timestamps, sorting, search/filter by portfolio or tag, and virtualization to handle 500+ communities smoothly. Includes keyboard navigation, screen-reader labels, and number formatting. Persists view state (selected KPI, sort, filters) in the URL for deep links and sharing. Integrates with the KPI API and respects user permissions.

Acceptance Criteria
Responsive Heatmap Grid Across Viewports
Given a viewport width between 320px and 480px When the heatmap loads Then each tile is at least 44x44px, no horizontal scrolling occurs, and the legend remains fully visible Given a viewport width of 768px or greater When the heatmap loads or the window is resized Then the grid auto-fits tiles to available width with consistent gutters and reflows within 250ms without layout shift Given device pixel ratio >= 2 When the grid renders Then text and tile borders remain crisp and KPI values are readable with compliant contrast
Accessible Color Scale Thresholds and Legend
Given configured thresholds for a KPI (OK, Watch, At-Risk) When community KPI values are rendered Then each tile’s state is determined against the thresholds and the correct color/state is applied Given the heatmap is displayed When colors are applied Then non-text contrast for state swatches is >= 3:1 against adjacent colors/background and states remain distinguishable under protanopia, deuteranopia, and tritanopia simulations Given a user cannot rely on color alone When they view the heatmap Then each state is also indicated by a unique pattern/icon in the legend and conveyed via state text in tooltips and screen-reader labels Given the legend is shown When thresholds are updated in configuration Then the legend updates within 500ms to reflect the new ranges and colors without a page reload
Tooltips Show Exact Values and Last Updated
Given a user hovers over or keyboard-focuses a tile When 200ms have elapsed Then a tooltip appears anchored to the tile showing: community name, KPI name, exact value (formatted), and last updated timestamp with timezone offset Given the tooltip is open When Escape is pressed or focus moves away Then the tooltip closes within 100ms without leaving visual artifacts Given values include percentages, currency, and counts When rendered in tooltips and tiles Then percentages show one decimal place (e.g., 87.3%), currency shows symbol and thousands separators (e.g., $12,345.67), and counts show thousands separators (e.g., 1,234) Given the tooltip would overflow the viewport When it is positioned Then it flips or offsets so it remains fully visible and does not cover the focused tile
Sorting and Search/Filter by Portfolio or Tag
Given communities with varying KPI values When the user sorts by the selected KPI ascending or descending Then tiles are ordered accordingly with ties broken by community name A–Z Given a text query is entered in Search When the user types at least 2 characters Then only communities whose names contain the query (case-insensitive) are shown; clearing the input restores the full set Given multi-select filters for Portfolio and Tag When portfolios and tags are selected Then the result includes communities in any selected portfolio AND with any of the selected tags; clearing either control updates the result immediately Given filters produce no results When the grid updates Then an empty state message appears with "No communities match your filters" and a Clear Filters action is available
Virtualization Performance for Large Portfolios
Given a dataset of 1,000 communities When the heatmap loads Then time to first interactive is <= 2 seconds on a standard laptop (4-core CPU, 8GB RAM) and CPU usage remains under 80% during initial render Given the user scrolls through the grid When continuously scrolling Then frame rate stays >= 50 FPS, no blank tiles are shown, and the DOM contains no more than 300 tile elements at any time Given keyboard navigation across virtualized content When moving focus past the last visible row Then additional rows are virtualized in seamlessly and focus continues without trapping or skipping
Keyboard Navigation and Screen Reader Support
Given the heatmap grid has focus When Arrow keys are pressed Then focus moves one tile in the corresponding direction; Tab moves to the next interactive control outside the grid Given a tile has focus When Enter or Space is pressed Then the tile’s tooltip opens; pressing Escape closes it and returns focus to the tile Given a screen reader user navigates tiles When focus lands on a tile Then it is announced as "Community Name, KPI Name, Value, State, Last updated <timestamp>" and the grid exposes name, row/column position, and total count via ARIA Given WCAG 2.1 AA requirements When testing focus visibility and reading order Then all interactive elements have a visible focus indicator and a logical, consistent reading order
URL State Persistence and Permissions Enforcement
Given the user changes KPI selection, sort direction, search query, portfolios, or tags When the change is made Then the URL query string updates (kpi, sort, dir, q, portfolios, tags) without a full page reload and is debounced within 300ms Given a deep link URL containing those parameters When loaded by any user Then the view (controls and results) restores to the encoded state subject to that user’s permissions Given the KPI API includes communities the user cannot access When results are rendered Then unauthorized communities are excluded and no identifiers for them appear anywhere in the UI Given browser navigation When Back or Forward is used Then the grid restores the corresponding state and results without a full reload
Threshold Configuration & At-Risk Surfacing
"As a manager, I want to set KPI thresholds per community so that at-risk communities surface automatically based on our goals."
Description

UI and backend to configure KPI thresholds at global, portfolio, and community levels with sensible defaults and templates. Thresholds drive tile colors, outlier badges, and the At-Risk list. Includes preview-before-save, change history (who/what/when), and validation to prevent conflicting rules. Supports per-KPI operators (e.g., ≥ for delinquency, ≤ for read rate) and effective dates. The heatmap consumes these settings in real time to surface communities requiring attention.

Acceptance Criteria
Threshold Precedence: Community Overrides Portfolio and Global
Given a global Read Rate threshold of ≤ 80%, a portfolio P Read Rate threshold of ≤ 75%, and a community C in P Read Rate threshold of ≤ 70% And community C has a Read Rate of 72%, another community D in P has 73%, and a community E in a different portfolio has 78% When the heatmap renders Then community C is evaluated against ≤ 70%, community D against ≤ 75%, and community E against ≤ 80% And tiles violating their effective threshold render Red with an Outlier badge; non‑violating tiles render Green with no badge And the At‑Risk list includes only tiles currently violating their effective threshold And after changing community C’s threshold to ≤ 75% and saving, its tile and At‑Risk status update within 3 seconds
KPI Operators Drive Color and Outlier Badges
Given KPI Delinquency uses operator ≥ with threshold 10% When a community’s Delinquency is 12% Then its tile is Red with an Outlier badge and the community appears in the At‑Risk list When the community’s Delinquency is 8% Then its tile is Green with no Outlier badge and it is not in the At‑Risk list Given KPI Read Rate uses operator ≤ with threshold 70% When a community’s Read Rate is 65% Then its tile is Red with an Outlier badge and the community appears in the At‑Risk list When the community’s Read Rate is 75% Then its tile is Green with no Outlier badge and it is not in the At‑Risk list
Preview-Before-Save Recalculates Without Persisting
Given an admin edits thresholds for Portfolio P but has not saved When the admin clicks Preview Then the heatmap and At‑Risk list recalculate using the edited values within 3 seconds And no change history entry is created and the edited values are not persisted When the admin refreshes the page or clicks Cancel Then the heatmap and At‑Risk list revert to the last saved thresholds When the admin clicks Save Then the heatmap and At‑Risk list match the previewed state and the changes persist
Effective Dates: Scheduled Threshold Activates in Real Time
Given Portfolio P has a current Delinquency threshold of ≥ 12% effective now And an admin schedules a new Delinquency threshold of ≥ 10% with an effective date/time T in the future and saves When now < T Then the heatmap and At‑Risk list use ≥ 12% When the clock reaches T Then within 5 seconds the heatmap and At‑Risk list switch to ≥ 10% without requiring a page reload And change history records both the scheduled change and its activation time
Change History: Log Who/What/When on Save
Given admin A updates Community C’s Read Rate threshold from ≤ 75% to ≤ 70% effective today and clicks Save When the save completes Then a change history entry is created capturing admin A’s identity, timestamp, scope (Community C), KPI (Read Rate), operator, previous value, new value, and effective date When admin B later changes the same threshold to ≤ 72% with a future effective date Then a new, separate history entry is created with admin B’s details and the scheduled date And history entries are immutable and viewable in chronological order for the scope and KPI
Templates and Defaults Applied at Scope
Given system templates Conservative and Moderate are available with predefined KPI thresholds When an admin applies the Conservative template to Portfolio P and clicks Save Then thresholds for all supported KPIs are set for P per the template, unless a community has an explicit override And newly created communities under P after this save inherit P’s template thresholds by default When the admin customizes one KPI value before saving the template Then the customized value persists instead of the template default
Conflict Validation: Prevent Overlapping or Duplicate Rules
Given an admin attempts to create two Read Rate thresholds at Portfolio P that both apply on the same effective date range When the admin clicks Save Then the save is blocked and an error states there can be only one active threshold per KPI and scope at any point in time Given an admin enters an operator not permitted for a KPI (e.g., sets Read Rate to ≥) When saving Then the save is blocked with an error indicating the allowed operator(s) for that KPI Given an admin enters an out‑of‑range value for a percentage‑based KPI (e.g., Read Rate 130%) When saving Then the save is blocked with an error indicating valid bounds (0–100%)
Drill-Down & Segmentation
"As a manager, I want to drill into a community’s KPI and segment it so that I can diagnose what is driving a change and take targeted action."
Description

Clicking a heatmap tile opens a detail view with KPI breakdowns by available segments (e.g., building, street, unit type, occupancy status, communication channel) and links to underlying announcements, invoices, and reminder logs. Supports filter combinations, breadcrumbs back to the heatmap, and deep links to pre-filtered views. Ensures pagination and lazy loading for performance, and redacts PII according to user permissions.

Acceptance Criteria
Drill-Down from Heatmap Tile to Segment Detail View
Given I am on the KPI Heatmap viewing KPI = Read Rate for a community and Date Range = Last 30 Days When I click that community’s heatmap tile Then a Segment Detail view opens showing KPIs broken down by Building, Street, Unit Type, Occupancy Status, and Communication Channel And the header reflects KPI = Read Rate and Date Range = Last 30 Days And the totals in the detail view match the tile’s totals at the time of click (no variance) And a breadcrumb like "KPI Heatmap > <Community> > Read Rate" is displayed
Multi-Segment Filtering and Combination Logic
Given the Segment Detail view is open When I apply filters Building in {A,B} and Unit Type in {Townhome} Then results include records where (Building = A OR Building = B) AND (Unit Type = Townhome) And the active filters are displayed as removable chips/tags And removing a chip updates the results immediately And an empty-state message appears if no records match the current filter set
Breadcrumb Navigation and State Preservation
Given I navigated to Segment Detail from the KPI Heatmap When I click the "KPI Heatmap" breadcrumb or use the browser Back button Then I return to the heatmap with my previous KPI selection, date range, thresholds, search, and scroll position preserved
Links to Underlying Records with Permissions
Given a Segment Detail breakdown row (e.g., Building A) is visible When I click "View Announcements", "View Invoices", or "View Reminder Logs" for that row Then I am taken to the corresponding list view pre-filtered by Community, KPI context, Date Range, and the selected segment value(s) And the record count in the list equals the count shown in the breakdown row at the time of navigation And if I lack permission to view a record type, the link is disabled with an explanatory tooltip; direct navigation attempts return 403 and show an access error
Deep Links to Pre-Filtered Views
Given I have a KPI, date range, community, and segment filters applied in Segment Detail When I copy/share the page URL and open it in a new session Then the same KPI, date range, community, and filters are restored And invalid or expired URL parameters are ignored gracefully with a non-blocking notice And if I lack permission for any filter scope, that scope is dropped and the view loads with remaining filters applied
Pagination and Lazy Loading Performance
Given a Segment Detail breakdown contains more than 50 rows When the view loads Then only the first 50 rows are fetched and rendered, with additional pages of 50 fetched via infinite scroll or "Load more" And each additional page loads within 400 ms at p95 for datasets up to 50,000 rows And a visible loading indicator is shown during page fetches And scrolling remains responsive (no main-thread task > 100 ms at p95)
PII Redaction per User Permissions
Given my role lacks "View PII" permission When I view Segment Detail or any linked list Then resident name, email, phone, full street address, and unit number fields are masked in the UI and omitted from exports/deep links And API responses exclude these fields (server-side redaction) And aggregate KPIs remain visible and unchanged And users with "View PII" see full values, and each access is recorded with user, timestamp, and scope in the audit log
Trends & Date Range Controls
"As a portfolio manager, I want to adjust the date range and see trends so that I can understand directionality and seasonality, not just a point-in-time value."
Description

Inline sparklines and trend indicators for each community/KPI with configurable date ranges (7/30/90 days, custom). Supports period-over-period comparisons and rolling averages to smooth noise. Displays anomaly markers when changes exceed configured thresholds. Date and timezone aware, with server-side aggregation for accuracy and client caching for responsiveness. Trend components reuse the KPI time-series API.

Acceptance Criteria
Select date range (7/30/90/custom) updates sparkline and trend
Given a portfolio manager is viewing the KPI Heatmap for a community and KPI When the user selects "Last 7 days" Then the sparkline and trend indicator update to show the last 7 full calendar days in the user’s timezone, ending at local 23:59:59 of the previous day And the number of plotted daily points equals 7 When the user selects "Last 30 days" or "Last 90 days" Then the series lengths equal 30 and 90 points respectively And the plotted values match the server-aggregated time-series API response for the same parameters When the user applies a valid custom date range (Start <= End) Then the series covers the inclusive start and end dates And the selected range is reflected in the UI control and tooltip timestamps When the user enters an invalid custom range (End < Start) Then the Apply action is disabled and a validation message is shown When switching between ranges Then a loading state appears during fetch and no stale data is displayed And the updated chart renders within 1.5 seconds on a 3G Fast profile for up to a 90-day range
Timezone-aware aggregation across DST
Given the user’s timezone is America/New_York and the selected range spans a DST transition When the series loads Then daily buckets align to local midnight boundaries with no duplicated or missing calendar days And the sum/count over the range equals the sum/count of raw events mapped to those local days Given the same KPI and date range When the user switches the app timezone to UTC Then the series re-aggregates to UTC bucket boundaries And totals are conserved while timestamps and bucket edges shift accordingly And the re-render completes within 2 seconds When the user’s timezone differs from the community’s default timezone Then the API request includes tz=the user’s timezone And the UI displays dates/times in the user’s timezone
Period-over-period comparison and percent change
Given a 30-day range and period-over-period comparison is enabled When the series loads Then the component fetches the prior 30-day baseline And displays the percent change with an arrow and color reflecting KPI polarity (improvement vs decline) Rule: Percent change is calculated as (current_mean - prior_mean) / abs(prior_mean), rounded to 1 decimal place; if prior_mean = 0, display N/A Given the baseline has less than 50% of expected data points When computing comparison Then the comparison badge displays N/A with a tooltip explaining insufficient data When the user clicks the comparison badge Then a tooltip shows current mean, prior mean, and absolute delta
Rolling average smoothing controls
Given rolling average smoothing is enabled with a default 7-day window When the series loads Then the sparkline overlays a 7-point moving average and retains the raw series (visually de-emphasized) And the legend indicates "7-day avg" When the user changes the smoothing window to 3 or 14 days Then the moving average recalculates and updates within 1 second And the legend reflects the selected window size Rule: Moving average values equal those returned by the time-series API for the specified window; if fewer points than the window exist, no value is plotted for those leading positions
Anomaly markers on threshold exceedance
Given a KPI has a configured relative anomaly threshold of 20% over the 7-day rolling average When a day’s value deviates by >= 20% Then an anomaly marker appears at that point in the sparkline and on the tile And the marker tooltip shows timestamp, actual value, baseline value, deviation %, and the applied threshold Given an absolute threshold is configured (e.g., +10 accounts) When the absolute deviation meets or exceeds the threshold Then an anomaly marker is shown Given no data points exceed any configured thresholds Then no anomaly markers are rendered Rule: Anomaly evaluation respects the current date range and smoothing window; markers are not rendered for data gaps; markers are keyboard-focusable with aria-label describing the anomaly
Server-side aggregation and client caching performance
Given a community, KPI, and date range are selected When the component requests data Then it calls the KPI time-series API with parameters community_id, kpi, start_date, end_date, tz, and rollup=day And the API returns pre-aggregated daily values used for plotting Rule: First render of a 90-day range completes within 2 seconds on a 3G Fast profile When the same parameters are requested again within 5 minutes Then the result is served from client cache and renders within 300 ms And no network fetch occurs or a conditional GET returns 304 via ETag/Last-Modified When the user triggers a manual Refresh Then the cache is bypassed and fresh data is fetched and rendered
Trend components reuse KPI time-series API
Given the KPI Heatmap trends are displayed When the component fetches data Then all trend visuals (sparklines, trend arrows, comparisons, anomalies) derive from the KPI time-series API responses And no direct queries to raw event streams are made by the client Rule: Requests target the configured time-series endpoint and include parameters for KPI, community, date range, timezone, and options (e.g., smoothing, comparison) When the API returns an error or is unavailable Then the component shows a non-blocking error state, avoids rendering stale data, and logs the failure for telemetry
Export & Sharing
"As a board member, I want to export and share the KPI view so that I can brief stakeholders who don’t use Duesly."
Description

Export the current heatmap view, filters, and trends to CSV/XLSX for data analysis and to PNG/PDF for visual reporting with legend, timestamp, and threshold notes. Offer scheduled email exports (weekly/monthly) to authorized recipients. All exports honor role-based access, include an audit trail entry, and avoid PII unless explicitly selected by an authorized user.

Acceptance Criteria
On-demand Data Export (CSV/XLSX) of Current Heatmap View
- Given an authorized user is viewing the KPI Heatmap with filters and a trend period applied, When they choose Export -> CSV, Then a CSV downloads containing one row per visible tile/segment with columns [community_id, community_name, read_rate_pct, payment_completion_pct, delinquency_rate_pct, reminder_effectiveness_pct, threshold_band, time_window_start_utc, time_window_end_utc, export_timestamp_utc, trend_period_days, filters_json, threshold_config_json] and values match the on-screen metrics within 0.1 percentage point. - Given the user chooses Export -> XLSX, When the export completes, Then the XLSX includes a 'Data' sheet with the same columns as the CSV and a 'Legend' sheet listing threshold bands and their color mappings. - Given no rows are visible due to filters, When the user opens the Export menu, Then CSV and XLSX options are disabled with a tooltip indicating 'No data to export'. - Given an export is triggered, When the file is generated, Then the file name follows the pattern Duesly-Heatmap-<format>-YYYYMMDD-HHMM-SSZ and the export_timestamp_utc column matches the timestamp in the file name.
Visual Export (PNG/PDF) with Legend, Timestamp, Threshold Notes
- Given an authorized user is viewing the KPI Heatmap with filters and trend period applied, When they choose Export -> PNG, Then the image includes the current viewport, a legend mapping colors to thresholds, an export timestamp (UTC), and threshold notes displayed in the footer. - Given the user selects Export -> PDF, When the PDF is generated, Then the PDF contains a single page mirroring the current viewport and includes the legend, export timestamp (UTC), threshold notes, and the active filters summary. - Given a PII exclusion policy is active (default), When exporting PNG or PDF, Then no PII fields (e.g., homeowner names, emails, unit identifiers) appear in the visual export. - Given the heatmap has been scrolled or zoomed, When exporting PNG or PDF, Then the output reproduces exactly the current on-screen view and filter state.
Scheduled Email Exports (Weekly/Monthly) to Authorized Recipients
- Given an authorized user with scheduling permission configures a weekly or monthly export, When they save the schedule, Then the system stores frequency, day/time, timezone, formats (CSV/XLSX/PNG/PDF), and recipient list and displays the next run time. - Given the scheduled time occurs, When the job executes, Then emails are sent only to recipients who are authorized per role-based access and the attachments/links reflect the current heatmap view and filters at execution time. - Given a recipient is unauthorized or becomes unauthorized, When saving a schedule or at send time, Then the system prevents delivery to that recipient and records the failure reason in the audit trail. - Given a scheduled export fails to send, When the job completes, Then the system retries up to 3 times with exponential backoff and records success/failure status per recipient in the audit trail.
Role-Based Access Enforcement for Exports
- Given a user lacks export permission for the current portfolio/community scope, When they open the Export menu, Then export options are hidden or disabled and any direct export API calls return HTTP 403 with no file produced. - Given a user has restricted scope (subset of communities), When they export, Then the export contains only data within their permitted scope and excludes out-of-scope tiles/segments. - Given a scheduled export exists, When it runs, Then it executes under the creator's effective permissions at run time and cannot expand scope beyond that access.
Audit Trail Logging for All Export Actions
- Given any export (manual or scheduled) is initiated, When the action completes (success or failure), Then an audit record is created containing: actor_id (or scheduler_id), actor_role, action_type (manual|scheduled), formats, scope_summary (portfolio/community IDs count), filters_hash, threshold_config_hash, trend_period_days, export_timestamp_utc, pii_override (true/false), recipients (for scheduled), result (success/failure), and error_code (if any). - Given an audit record is created, When queried via the audit log UI/API, Then it is visible within 60 seconds and immutable thereafter. - Given a user attempts an unauthorized export, When the attempt is blocked, Then an audit record is still created with result=failure and reason=unauthorized.
PII Exclusion by Default with Explicit Authorized Override
- Given default export settings, When any export is produced, Then PII fields (e.g., homeowner names, emails, phone numbers, unit identifiers, street addresses) are excluded from CSV/XLSX columns and from any PNG/PDF annotations. - Given a user with the 'Export PII' permission explicitly enables 'Include PII' for an export, When the export is generated, Then only the additional PII fields selected by the user are included and the audit record sets pii_override=true. - Given a user without the 'Export PII' permission, When they attempt to enable 'Include PII', Then the control is not visible or is disabled and the export proceeds without PII. - Given a scheduled export is configured with PII, When saving the schedule, Then the system validates that all recipients are authorized to receive PII; otherwise saving is blocked with an error message and no schedule is created.
Filter, Segment, and Trend Preservation in Exports (Including Drilldowns)
- Given the user has applied filters and drilled into a segment from the heatmap, When they export from that state, Then the export contains only the drilled segment's data and includes a segment_definition field summarizing the drill criteria. - Given trends are visible on the heatmap, When exporting CSV/XLSX, Then trend metrics (e.g., read_rate_delta_pct, payment_completion_delta_pct, delinquency_rate_delta_pct) are included as columns and match the on-screen deltas. - Given active filters, When exporting PNG/PDF, Then an 'Active Filters' summary appears in the footer and matches the current filter set. - Given the export is opened by an analyst, When they aggregate exported data, Then computed totals/averages match the heatmap's displayed aggregates within 0.1 percentage point for the same filter scope.
Role-Based Access & Data Security
"As an admin, I want access scoped by role and portfolio so that sensitive community data remains protected while managers get the insights they need."
Description

Enforce RBAC across the heatmap API and UI so users only see communities and KPIs in portfolios they are permitted to access. Apply row-level filters to queries, redact resident-level identifiers in drill-downs by default, and log access events for auditing. Use encrypted transport and storage for KPI aggregates, and ensure configuration UIs are limited to admins. Integrates with Duesly’s existing authentication and organization model.

Acceptance Criteria
RBAC: Portfolio-Scoped Heatmap Visibility (UI)
Given a user with access to portfolios P1 and P2 containing communities {C1, C2, C5} When they open the KPI Heatmap UI Then only tiles and aggregates for {C1, C2, C5} are rendered and no other communities appear And the totals/averages displayed reflect only authorized communities Given a user with no permitted portfolios When they open the KPI Heatmap UI Then a “No accessible data” state is shown and no KPIs or tiles are rendered Given a direct URL including an unauthorized community ID When the page loads Then no data for that ID is shown and the user is notified they lack access
RBAC: API Enforcement and Row-Level Filters
Given a valid auth token for org X with portfolio scope {P1} When calling GET /api/heatmap?kpis=read_rate,payment_completion Then the response includes only communities within {P1} and excludes all others And the response meta includes counts (e.g., total_communities_returned) matching the authorized set size Given a request referencing community IDs outside the caller’s scope When the API is called Then a 403 Forbidden is returned and no partial data is leaked Given the caller’s permissions are revoked between requests When the next API call is made Then access is denied (401/403) and no out-of-scope rows are returned
Drill-Down Views and Exports Redact Resident Identifiers by Default
Given a user drills into a heatmap tile to view segment details When the detail table loads Then resident-level identifiers (name, email, phone, exact street address, unit number) are omitted or masked and cannot be reconstructed client-side And any unique resident references are represented by non-reversible anonymous tokens Given the user exports from this drill-down view When the CSV/XLSX is downloaded Then it contains only redacted or aggregated fields with no resident identifiers Given a bookmarked deep link to a drill-down When opened Then redaction rules are still enforced and no identifiers are revealed
Comprehensive Access Auditing
Given any Heatmap UI view or API endpoint is accessed (success or denial) When the request completes Then an audit event is persisted containing timestamp, user_id, org_id, portfolio_ids, resource, action, result (success/denied), and request IP/user-agent And export actions record the file type, row count, and KPI set in the audit event Given an admin queries the audit log for a specific user/time window When requesting via the audit API Then the corresponding events for heatmap access are returned
Encryption in Transit and at Rest for KPI Aggregates
Given any heatmap API endpoint When accessed over HTTP Then the request is refused or redirected to HTTPS and HSTS is enabled Given TLS inspection of the endpoint When negotiated Then the protocol is TLS 1.2 or higher and strong ciphers are used Given KPI aggregate storage and backups When inspected via platform encryption flags/metadata Then encryption-at-rest is enabled for primary storage and backups/snapshots And application code and infrastructure do not write plaintext KPI aggregates to unencrypted locations (temp files, logs)
Admin-Only Heatmap Configuration UI
Given a non-admin user attempts to access Heatmap configuration (thresholds, segment definitions, exports policy) When navigating via URL or UI Then access is blocked (403/404) and navigation links are not visible Given an admin user accesses the configuration UI When updating thresholds and saving Then changes are persisted, an audit event is created, and the new thresholds apply only within the admin’s organization scope Given a non-admin attempts to call configuration APIs directly When the request is made Then the API returns 403 and no changes occur
Integration with Duesly Authentication and Organization Model
Given a request without a valid Duesly auth token When calling Heatmap UI or API Then a 401 Unauthorized is returned and no data is rendered Given a user belongs to org X with portfolio membership {P1,P2} When accessing heatmap data Then scoping uses org X and {P1,P2}; removing the user from P2 immediately prevents access to P2 data on the next request Given an SSO session expires or a token is revoked When the next API/UI request occurs Then the session is invalidated and no heatmap data is returned until re-authentication

Bulk Guardrails

Run a dry‑run validation before any bulk action. Catch duplicates, date collisions, missing ledger codes, or audience conflicts. Require approvals for large sends, sample a small cohort first, and keep a one‑click rollback with versioned change logs. Fewer mistakes, faster recovery if something slips.

Requirements

Dry-Run Bulk Validation
"As a volunteer board treasurer, I want to run a dry-run check for a bulk billing action so that I can catch errors and fix them before anything is sent or posted."
Description

Execute a no-side-effects simulation of any bulk action (post-to-bill conversions, announcements, reminders, compliance notices) and aggregate findings before execution. The validator inspects duplicates, date collisions, missing or invalid ledger codes, inactive owners/units, audience conflicts, permission gaps, and scheduling windows. Results are severity-tagged with counts, affected records, and suggested fixes, visible in the feed preflight panel and via API for automation. Each validation report is stored, shareable with approvers, and bound to the pending change set to ensure traceability.

Acceptance Criteria
Dry-Run Produces No Side Effects
Given I initiate a dry-run for a supported bulk action When the dry-run completes Then no domain records (owners, units, posts, bills, notices, payments, reminders) are created, updated, or deleted And no notifications, emails, pushes, or webhooks are emitted And only a ValidationReport record and its child ValidationFindings are persisted And the system logs the dry-run with a correlation ID without altering business data
Duplicate and Date Collision Detection for Post-to-Bill Conversion
Given a bulk post-to-bill conversion containing candidate items with duplicate external IDs or invoice numbers When I run a dry-run Then the report includes findings with severity "Error" for each duplicate group And each finding lists the affected record IDs and duplicate keys And the report aggregates a duplicates count by severity and action type Given candidate items with due dates colliding with existing unpaid invoices for the same owner/unit and ledger code When I run a dry-run Then the report includes findings with severity "Warning" for date collisions And each finding suggests a latest non-colliding due date based on configured spacing rules
Ledger Code Validation with Severity and Suggestions
Given a bulk action referencing ledger codes When I run a dry-run Then missing ledger codes are reported with severity "Error" And invalid or inactive ledger codes are reported with severity "Error" And deprecated-but-mapped ledger codes are reported with severity "Warning" including the mapped target code And each finding includes the affected record IDs and a suggested fix (create code, reactivate, or replace with {code})
Audience and Permission Validation
Given a bulk announcement/reminder targeting an audience When I run a dry-run Then inactive owners or archived units are listed with severity "Warning" and excluded counts And owners without a valid delivery channel are listed with severity "Warning" per channel And audience conflicts (e.g., overlapping segments causing duplicate delivery) are listed with severity "Warning" including deduplicated totals And operator permission gaps (missing role to bill/notify selected audience) are listed with severity "Error" with the required permission name
Scheduling Window and Blackout Check
Given a bulk action scheduled for a specific datetime When I run a dry-run Then if the datetime falls outside the community’s allowed send window, the finding severity is "Error" and includes the next available window start And if the datetime overlaps a configured blackout period, the finding severity is "Error" and includes the blackout label and end time And if the datetime is within quiet hours, the finding severity is "Warning" and includes a recommended reschedule time And the report totals reflect counts by window violation type
Validation Report Persistence, Shareability, and Binding
Given a completed dry-run When I view its stored ValidationReport Then it has an immutable ID, createdAt timestamp, actionType, actorId, severityBreakdown, counts, and boundChangeSetId And it is read-only and versioned; subsequent re-validations create a new version linked via priorReportId And a shareable link is generated that requires approver permission to access And when any parameter or member of the bound change set is modified, the previous report is marked "Stale" and execution is blocked until a fresh dry-run exists for the current change set
Preflight Panel and API Output Consistency
Given I open the preflight panel for a pending bulk action with a completed dry-run When the panel renders Then it displays severity badges (Error, Warning, Info) with counts matching the stored report And I can filter findings by severity and type, and export affected records as CSV And the Execute button is disabled if Error count > 0 and enabled if only Warnings/Info remain Given I fetch the report via the public API endpoint /v1/validation-reports/{id} When the response returns Then it is application/json with fields: id, actionType, boundChangeSetId, status, severityBreakdown, counts, findings[], suggestedFixes[], createdAt, expiresAt And findings[] include id, severity, type, affectedRecordIds[], message, suggestion And the API response matches the data shown in the preflight panel
Threshold-Based Approval Workflow
"As a property manager, I want large bulk sends to require approval so that we reduce the risk of accidental mass billing or messaging."
Description

Enforce configurable approval rules for high-impact bulk actions based on recipient count, total dollar exposure, schedule time, or audience type. Provide multi-step approvals with named approver groups, delegation, SLAs, reminders, and block-until-approved gating. The approval view presents the dry-run report, impact summary, and diffs; approvers can comment, request changes, or approve with logged overrides. All actions append to an immutable audit trail and are exportable for compliance and board review.

Acceptance Criteria
Approval Trigger by Recipient Count Threshold
- Given a configured recipient_count_threshold T, When a bulk action targets M recipients where M >= T and the requester clicks Submit, Then an approval request is created, the action status becomes 'Pending Approval', and execution is blocked until approval. - Given M < T and all other validations pass, When the requester clicks Submit, Then no approval request is created and the action proceeds to scheduling/execution. - Given an approval request exists and the target audience changes, When M recalculates, Then thresholds are re-evaluated and the request is marked 'Needs Re-approval' if M >= T or auto-unblocked if M < T, with both changes logged in the audit trail. - Given a configured pilot_sample_threshold P and pilot_size S, When M >= P, Then the system requires a pilot cohort send of size S and withholds final approval until pilot results are recorded or an override with a mandatory reason is submitted by an authorized approver.
Total Dollar Exposure Approval Threshold
- Given a configured dollar_exposure_threshold D and currency C, When the dry-run computes total_exposure E in currency C and E >= D, Then the system requires approval and displays E and D clearly in the approval request. - Given the dry-run contains items with missing ledger codes, When the requester submits for approval, Then 'Approve' is disabled and only 'Request Changes' or 'Approve with Override' is available; selecting override requires choosing the violated rule(s) and entering a non-empty reason. - Given total_exposure E changes due to edits, When the requester resubmits, Then the approval view shows a diff of exposure change (old_E -> new_E) and logs the change in the audit trail.
Multi-Step Approval by Named Approver Groups and SLA
- Given an approval policy defines ordered approver_groups [G1..Gn] with min_approvals per group, When a request is created, Then the system routes to G1 and requires min_approvals(G1) before routing to G2, continuing until Gn is satisfied. - Given an approver has an active delegate D, When D approves, Then the decision is recorded as delegated with both principal and delegate identities and counts toward the group's min_approvals. - Given SLA_hours H for a group step, When H elapses without meeting min_approvals, Then reminders are sent at configured intervals, the step is marked 'Escalated', and routing proceeds per the group's escalation policy; all notifications are logged.
Block-Until-Approved Gating and Schedule Window Enforcement
- Given an action has status 'Pending Approval' or 'Changes Requested', When the scheduled time arrives, Then the action does not execute and the requester is notified that approval is required; the schedule remains intact but paused. - Given an action is fully approved after its scheduled time has passed, When the requester resumes, Then the system requires either a new scheduled time or an explicit 'Execute Now' confirmation before sending. - Given a configured min_approval_lead_time L, When the requester attempts to schedule within L without approvals complete, Then scheduling is blocked with a clear error and a link to the approval request.
Approval Review UI Shows Dry-Run, Impact Summary, and Diffs with Actions
- Given an approval request is opened, Then the view displays: dry-run validation results (errors/warnings), recipient count, total dollar exposure, schedule time, audience segments, and a diff of all changes since last submission. - Given the approver chooses 'Request Changes', Then a non-empty comment is required, the requester is notified, and the request status changes to 'Changes Requested' with the comment visible in the thread. - Given the approver chooses 'Approve', Then the decision is logged with user, time, approver group, and version hash; if any critical validation errors exist, 'Approve' is disabled and only 'Approve with Override' is enabled. - Given the approver chooses 'Approve with Override', Then they must select one or more violated rules and enter a reason; the override is logged and surfaced in exports.
Immutable Audit Trail and Export
- Given any action on an approval request (create, comment, change, approve, reject, override, delegate), Then an append-only audit entry is written with ISO 8601 UTC timestamp, actor ID, role, action type, request version hash, and before/after snapshots where applicable. - Given a user with Compliance Export permission, When they export an approval workflow, Then the system generates CSV and JSON containing the full audit trail, approval decisions, overrides, diffs, and applicable configuration-at-decision; the export event is logged with a unique export ID. - Given filter parameters (date range, status, requester, audience), When applied to export, Then the exported dataset includes only matching workflows.
Audience-Type and Off-Hours Approval Rules
- Given a policy that marks audience types [All Units, Board Members, Delinquent > 90d] as high risk, When a bulk action targets any high-risk audience, Then approval is required by the designated Board approver group regardless of other thresholds. - Given community business hours and a holiday calendar, When a bulk action is scheduled outside business hours or on a holiday, Then an additional 'Officer' approval is required or an override with a mandatory reason must be provided; the community's configured timezone is used for evaluation. - Given off-hours detection is enabled, When the scheduled time changes, Then the system re-evaluates the rule and updates required approver groups accordingly, logging any additions or removals.
Canary Cohort Execution
"As a board secretary, I want to send to a small test cohort first so that I can confirm everything works before reaching the full community."
Description

Allow bulk actions to run first against a small, representative sample (configurable percentage or count) prior to full rollout. Monitor health metrics (delivery errors, bounce rates, payment link issues, validation warnings) and auto-pause if thresholds are exceeded. After a successful canary, auto-promote to full send or require manual continue. Support scheduled windows, timezone awareness, and per-community throttling to respect provider rate limits and avoid resident spam.

Acceptance Criteria
Configure canary cohort by percentage or count
Given a bulk action with total audience size N and canary mode enabled When the operator sets the canary size as P% or C recipients and saves Then the system selects floor(N*P/100) recipients when P% is used, or min(C, N) recipients when C is used And the selection is deterministic per action (same cohort on rerun unless audience changes) And the selected cohort contains no duplicates and respects all audience filters and exclusions
Real-time health monitoring and auto-pause on threshold breach
Given metric thresholds are configured for the action (delivery_error_rate<=2%, bounce_rate<=3%, payment_link_error_rate<=1%, validation_warnings=0 by default) When the canary executes and metrics are computed continuously over attempted deliveries Then if any metric exceeds its threshold before canary completion, the rollout auto-pauses within 60 seconds And the system prevents any transition to full send, marks status "Paused - Canary Failed", and notifies owners via in-app and email with a metrics snapshot and timestamp
Auto-promote to full rollout upon successful canary
Given the canary completes and all monitored metrics are at or below thresholds And the action is configured for Auto-Promote When the canary completes Then the system immediately queues the full rollout, honoring schedule windows and throttling rules And records an audit log entry with decision=Auto-Promote, metrics summary, timestamp, and actor=system
Manual continue required after successful canary
Given the canary completes and all monitored metrics are at or below thresholds And the action is configured for Manual Continue When an authorized user clicks "Continue to Full Send" Then the system presents a review screen showing metrics, audience size, and estimated duration and requires confirmation And proceeds only after confirmation, recording user, timestamp, and metrics in the audit log
Scheduled windows and timezone-aware execution
Given an action is scheduled to run only between 09:00 and 17:00 in timezone TZ When the canary start time falls outside the allowed window in TZ Then the canary delays until the next allowed window And daylight saving time transitions in TZ are respected so that sends never occur outside 09:00–17:00 local time And the subsequent full rollout also respects the same window constraints
Per-community provider throttling and backoff
Given provider rate limits of R sends per minute per community When the canary and full rollout execute Then the system never dispatches more than R sends per minute for any single community And on 429/rate-limit responses, the system applies exponential backoff and retries without exceeding R And overall queuing preserves FIFO order within each community
One-click rollback of paused canary changes
Given a canary has been auto-paused or manually paused after partial application to recipients When an authorized user clicks "Rollback Canary" Then the system reverts all canary-applied changes atomically where possible (e.g., retracts announcements, voids unpaid invoices, reverses ledger entries) and stores versioned change logs And no new notifications are sent to recipients during rollback And the action status updates to "Rolled Back" with a report of items reversed
One-Click Rollback with Versioned Change Sets
"As a treasurer, I want a one-click rollback of a bulk billing mistake so that I can quickly recover and keep the books accurate."
Description

Snapshot target records and compose a versioned change set prior to executing a bulk action, enabling a single action to reverse effects. Rollback operations include voiding invoices, reversing ledger entries with ties to originals, retracting announcements, and canceling scheduled reminders. The system reconciles accounting impacts to preserve auditability and prevents double-posting. Every execute/rollback records actor, timestamp, reason, and diffs in an immutable change log.

Acceptance Criteria
Pre-Execution Snapshot and Versioned Change Set Created
Given a bulk action is confirmed for execution When the system prepares to execute Then it creates a versioned change set with a unique versionId and idempotency hash, actorId, timestampUTC, and reason And persists a read-only snapshot of all target record identifiers and pre-change values And records intended mutations for each target record And stores snapshot and change set atomically before any mutation occurs And the snapshot is retrievable via API and UI detail view for the versionId And for up to 5,000 records the snapshot creation completes within 30 seconds
Immutable Change Log for Execute and Rollback
Given a bulk execute or rollback completes When the change log for the versionId is queried Then an append-only entry exists with actorId, timestampUTC, reason, operationType (execute|rollback), itemized diffs, and cross-links to related operations And entries are non-editable and non-deletable via API and UI And any modification attempt is denied with 403 and is itself logged with actorId and timestampUTC And the itemized diffs accurately reflect field-level changes between snapshot and current state
One-Click Rollback of Bulk Invoice Creation
Given a previously executed change set created invoices, ledger entries, reminders, and announcements When an authorized user clicks Rollback for that versionId and provides a reason Then the system voids all invoices created by that change set And posts reversing ledger entries equal in amount and opposite in sign, linked to originalEntryId and versionId And cancels all scheduled reminders and retracts associated announcements from recipient feeds And restores target records to their pre-execution values as per the snapshot And marks the change set status as Rolled Back with completion timestamp And for up to 5,000 invoices the rollback completes within 2 minutes
Partial Rollback with Posted Payments or External Locks
Given some records from the change set cannot be voided due to posted payments or external locks When rollback is triggered for the versionId Then the system does not delete or void those locked records And posts corrective ledger entries to net the accounting impact to zero while preserving existing payments And links all corrective entries and affected records to the rollback versionId And adds a rollback note on each exception record with reason and timestamp And produces a downloadable report (CSV and UI) listing each exception, cause, and remediation status And marks overall rollback as Partial until exceptions are resolved or explicitly dismissed by an administrator
Announcement Retraction and Reminder Cancellation on Rollback
Given the execute created announcements and scheduled reminders When rollback is completed for the versionId Then announcements created by that change set are no longer visible to recipients and display Retracted with reference to the versionId for admins And all future-dated reminders from the change set are canceled and removed from send queues And no reminders are sent after the rollback timestamp for that versionId And already-sent reminders remain in history and are linked to the versionId with a sent prior to rollback label
Idempotent Re-Execution Without Double-Posting
Given a change set has been executed and rolled back When the same payload is submitted with the same idempotency hash within 30 days Then the system rejects the request as a duplicate and surfaces the prior versionId And no new invoices, ledger entries, announcements, or reminders are created And when the payload is submitted with a new versionId and idempotency hash Then the system executes without creating duplicates of records previously created and rolled back
Performance and Telemetry for Snapshot and Rollback
Given a bulk change set affecting up to 5,000 records When snapshot creation and rollback run Then snapshot creation completes within 30 seconds and rollback completes within 3 minutes And real-time progress telemetry is exposed via UI and API with updates at least every 10 seconds And transient failures are retried up to 3 times with exponential backoff before being marked as exceptions in the rollback report And overall operation success criteria is met when ≥99.9% of eligible records are processed without error
Ledger Code and Accounting Guardrails
"As an accountant advisor, I want guardrails on ledger codes so that bills post correctly and our financial reports remain accurate."
Description

Validate that every bill item maps to an active ledger code, tax profile, and GL account per community policy before posting. Enforce required accounting fields (e.g., class, cost center, fund), block deprecated mappings, and flag cross-period postings or closed-period dates. Provide autosuggestions based on prior usage and surface a clear accounting summary in the dry-run, marking blockers that must be resolved to proceed.

Acceptance Criteria
Validate Active Accounting Mappings During Dry-Run
Given a bulk bill draft with one or more bill items And the community has active ledger codes, tax profiles, and GL accounts configured When the user runs the dry-run validation Then each bill item must have an active ledger code, tax profile, and GL account mapping And any item missing or mapped to inactive entities is flagged as Blocker with item identifier and reason And the Post action is disabled until all blockers are resolved And the dry-run summary displays counts of blockers by type
Block Deprecated Ledger Code and Account Mappings
Given community policy marks certain ledger codes, tax profiles, or GL accounts as deprecated When a bill item references a deprecated mapping during dry-run Then the item is flagged as Blocker and cannot proceed to post And the system suggests the closest active mapping if available And selecting a suggested mapping removes the blocker and updates the item
Enforce Required Accounting Fields (Class, Cost Center, Fund)
Given community accounting policy marks class, cost center, and fund as required fields When the user runs dry-run validation on a bulk bill Then any item missing any required field is flagged as Blocker with the missing field name And items with all required fields pass this rule And the Post action remains disabled while any such blocker exists
Flag Cross-Period and Closed-Period Dates
Given the community has accounting periods with statuses (open, closed) And the bulk bill contains item dates or posting periods outside the current open period When the user runs dry-run validation Then items with closed-period dates are flagged as Warning with period status and date And items that cross periods contrary to policy are flagged as Warning And if policy requires approval for warnings, the system requests an approver before posting And all warnings appear in the dry-run summary and audit log
Autosuggest Accounting Mappings Based on Prior Usage
Given historical bills exist in the community And a new bill item is missing a ledger code, tax profile, or GL account When the user views the dry-run errors Then the system displays up to three suggested mappings per missing field with confidence scores And selecting a suggestion fills the field and revalidates the item in under 2 seconds And acceptance is met when at least 80% of similar items receive a suggestion
Dry-Run Accounting Summary and Export
Given a dry-run has completed When the user opens the accounting summary Then totals are shown by ledger code, tax profile, GL account, and required fields And blockers and warnings are grouped with counts and links to affected items And the user can export the summary to CSV including item-level reasons And the summary matches the item set within ±0.01 monetary rounding
Re-Validation at Post Time and Race Condition Guard
Given a dry-run passed with no blockers And time has elapsed during which accounting configurations may have changed When the user clicks Post Then the system re-runs validation server-side immediately before commit And if new blockers are detected (e.g., ledger code deactivated), the post is aborted and results are shown And no partial posting occurs; either 0% or 100% of items are posted in the batch
Audience and Schedule Conflict Checker
"As a community manager, I want to prevent audience overlaps and timing collisions so that residents don’t receive duplicate or conflicting communications."
Description

Detect overlapping audience segments and scheduling conflicts across campaigns, dues cycles, and compliance notices. Prevent double-sends to the same owner/unit within a configurable window, honor opt-outs and channel preferences, and exclude ineligible recipients (inactive, transferred, or restricted). Provide a preview count with deltas from previous runs and a breakdown by building, unit type, and delivery channel to ensure accurate targeting.

Acceptance Criteria
Double-Send Prevention Within Configurable Window
Given a duplicate-prevention window of N days is configured And Campaign A is scheduled to send via Email to Owner O for Unit U at T0 And Campaign B targets the same Owner O and Unit U via Email at time T1 such that 0 <= T1 - T0 < N days When the dry-run validation is executed for Campaign B Then Owner O / Unit U is excluded from Campaign B And the preview displays a conflict count >= 1 with reason "Duplicate within window" And the total eligible recipient count decreases by the number of excluded duplicates And the validation report lists the conflicting Item IDs (e.g., Campaign/Notice/Dues cycle) for each excluded recipient
Cross-Campaign Audience Overlap Detection
Given two bulk actions (Action X and Action Y) have audience rules that overlap (e.g., balance > $0 and violation in last 30 days) And their scheduled send windows overlap by at least 1 minute When a dry-run is executed for Action Y Then the system calculates the set of overlapping recipients between X and Y And displays the overlap count and percentage relative to Y And provides a downloadable/expandable list of overlapping recipients with Owner ID, Unit ID, Building, and Channel And marks Action Y as "Conflict Detected" until the overlap is resolved or the schedule is adjusted
Opt-Outs and Channel Preferences Enforcement
Given recipients may have per-channel preferences and opt-outs (Email, SMS, Push, Paper) And a bulk action targets multiple channels When the dry-run validation is executed Then any recipient opted out of a channel is excluded from that channel for this action And any recipient with a preference restricting delivery to a subset of channels only receives via allowed channels And the preview shows per-channel excluded counts with reason codes ("Opt-out", "Channel not preferred") And no excluded recipient appears in the final per-channel eligible list
Recipient Eligibility Filters (Inactive, Transferred, Restricted)
Given the directory contains recipients flagged as Inactive, Transferred, or Restricted When a dry-run validation is executed for a bulk action Then recipients with any of these ineligibility flags are excluded across all channels And the preview shows excluded counts by ineligibility reason And the total eligible count equals total targeted minus excluded (duplicates + opt-outs + ineligible) And an audit line item for each excluded recipient includes the exclusion reason and timestamp
Schedule Collision Across Dues Cycles and Compliance Notices
Given a dues cycle reminder and a compliance notice are both scheduled to the same Owner O / Unit U within the same 24-hour window When the dry-run validation is executed for the later-scheduled item Then a scheduling conflict is flagged with type "Same recipient within 24h" And the system recommends the next available send time outside the configured collision window And the preview indicates how many recipients are affected and the proposed reschedule delta And the later-scheduled item is blocked from final send until the conflict is resolved or an authorized override is recorded
Preview Counts, Deltas, and Breakdown Accuracy
Given a dry-run was executed previously for Action Z and stored as Version V1 with its counts And the underlying data or configuration has changed When a new dry-run is executed for Action Z producing Version V2 Then the preview displays Added, Removed, and Unchanged recipient counts (V2 vs V1) And provides breakdowns by Building, Unit Type, and Delivery Channel whose totals reconcile to the V2 eligible count And the delta view shows reason-coded changes for at least 95% of differences (e.g., new opt-out, duplicate window, ineligible) And all counts are exportable with matching totals between on-screen and exported data

Override Panel

Apply a global action, then fine‑tune per‑community in one side‑by‑side view. Edit amounts, dates, channels, and audiences inline; exempt a community; or save an exception as a reusable rule. Every override records who/why for a clear audit trail and consistent governance.

Requirements

Side-by-Side Override Workspace
"As a board admin managing multiple communities, I want a single screen to apply a global change and then fine‑tune each community side-by-side so that I can move fast while ensuring local accuracy."
Description

Provide a unified Override Panel that applies a single global action (e.g., update dues notice, change reminder cadence, adjust amount) and presents all communities in a side-by-side grid to fine-tune per-community variations. The workspace must show default (global) values alongside community-specific overrides with clear diffing, filtering, and pagination for large portfolios. It supports draft mode, unsaved-change indicators, keyboard navigation, and bulk-select editing, and integrates with Duesly’s feed so overrides map directly to the originating post/bill. The grid must surface columns for amount, due date, channels (feed, email, SMS, mail), audience segments, and reminder policy, and persist draft state until applied or discarded.

Acceptance Criteria
Apply Global Action with Side-by-Side Defaults and Overrides
Given a user selects a global action and sets default values When the Override Panel opens Then the grid lists all communities in a paginated table with columns: Amount, Due Date, Channels, Audience Segments, Reminder Policy, Override Status And a Default column is displayed adjacent to each editable column showing the global values And any cell whose value differs from the Default is flagged as Overridden via icon and highlight And an Overridden counter displays the correct number of rows currently differing from Default
Draft Mode Persistence and Unsaved Change Indicators
Given a user edits one or more cells without applying When they navigate away and return within 24 hours or refresh the browser Then all unsaved edits are restored as a draft And each modified cell shows an unsaved-change indicator And the Apply button is enabled only if there are no validation errors in the selection And clicking Discard removes all draft edits and clears indicators
Filtering, Sorting, Diff View, and Pagination at Scale
Given a portfolio with at least 1,000 communities When the user applies filters (Overridden, Exempted, Validation Errors, Channel, Audience, Reminder Policy, Due Date range) and sorting on any column Then the grid shows only matching rows with accurate counts and preserves unsaved edits And the user can toggle Show differences only to hide rows matching defaults And pagination defaults to 50 rows per page and supports 25/50/100; each page loads in under 2 seconds on first view and under 1 second from cache
Bulk-Select Editing, Exemptions, and Save as Reusable Rule
Given the user bulk-selects N rows When they edit a supported field via the bulk toolbar and preview changes Then exactly N rows reflect the pending change with per-row diff highlights And the user can mark selected rows as Exempt so they are excluded from Apply And the user can Save as reusable rule by providing name, scope, and reason; the rule appears in the Rule Library and auto-applies to future matching global actions And the Apply button shows the exact count of rows to be updated and excludes Exempt rows
Channel, Audience, Amount, Due Date, and Reminder Policy Validation
Given the user edits channels, audience, amount, due date, or reminder policy When values violate constraints Then inline validation prevents Apply and displays per-cell errors: amount requires currency and non-negative; due date cannot be in the past; SMS requires opt-in; email requires a deliverable address; mail requires a mailing address; audience cannot be empty; reminder policy must be a valid template And a banner summarizes error counts and provides a filter to show only rows with errors
Keyboard Navigation and Accessibility Compliance
Given the grid has focus When the user navigates using keyboard Then arrow keys move cell focus, Enter toggles edit mode, Escape cancels edits, Tab moves to next editable cell, Space toggles checkboxes, and Shift+Click or Shift+Arrow supports range selection And visible focus indicators are present; all controls have accessible names/roles/states; contrast ratios meet WCAG 2.1 AA; and screen readers announce Default versus Override values
Apply/Discard Workflow with Feed Mapping and Audit Trail
Given there are valid pending edits When the user clicks Apply and confirms with a required reason note Then overrides are persisted and reflected in each community’s feed item mapped to the originating post/bill And an audit record is created per community capturing user, timestamp, fields changed (before/after), global action ID, reason, and any rule IDs used, and is retrievable via UI/API And if any row fails to apply, the operation is partial: successes persist; failures are listed with error messages and a Retry failed action is available And when the user clicks Discard, no changes persist and no audit records are created
Inline Field Editing
"As a treasurer, I want to edit amounts and dates inline with real-time validation so that I can correct exceptions quickly without navigating away."
Description

Enable inline, spreadsheet-like editing for key fields (amounts, due dates, delivery channels, audience segments, reminder cadence) directly in the grid with immediate validation and helpful error messages. Support currency formatting, min/max constraints, prohibited negative values, timezone-aware dates, channel availability per community, and audience membership previews. Allow multi-row edit via bulk selection, quick copy/paste, and per-column bulk apply. Changes should update row-level status (e.g., Valid, Needs Attention) and calculate impact summaries in real time. Integrate with Duesly’s billing engine to reflect constraints from existing invoices or compliance holds.

Acceptance Criteria
Inline Currency Validation and Formatting
Given a community row with the Amount field in inline edit mode When the user enters a valid numeric value with or without currency symbols Then the value auto-formats to the community’s currency and locale and passes validation Given the Amount field is edited When the input is negative, non-numeric, exceeds the system-configured maximum, or is below the minimum Then an inline error message explains the rule violated, the field is highlighted, the row status becomes "Needs Attention," and the change is not committed Given an Amount field previously in error When the user corrects the value within allowed constraints Then the error clears, the row status returns to "Valid," and the impact summary recalculates immediately
Timezone-Aware Due Date Editing
Given a community row configured with a specific timezone When the user edits the Due Date inline Then the date picker displays and validates in the community timezone and the stored value is normalized correctly without off-by-one day errors Given the Due Date field is edited When the user enters a date outside system-configured min/max bounds or an unparsable format Then an inline error describes the issue, the row status becomes "Needs Attention," and the invalid value is not committed Given a valid Due Date is entered When the edit is committed Then the updated Due Date is reflected consistently across the grid and summaries in the community’s timezone
Delivery Channel Availability Per Community
Given a community row and the Delivery Channel selector in inline edit mode When the user opens the selector Then only channels available to that community are selectable; unavailable channels appear disabled with a tooltip explaining why Given a user attempts to select an unavailable channel When the selection is made Then the selection is blocked, an inline message explains the constraint, and the row status becomes "Needs Attention" Given a valid available channel is selected When the edit is committed Then the selection is saved, the row status is "Valid," and any dependent fields update if required
Audience Preview and Reminder Cadence Validation
Given a community row and the Audience field in inline edit mode When the user selects an audience segment Then a preview shows member count and sample members for that community in real time; if the count is zero, the row status becomes "Needs Attention" with a warning Given the Reminder Cadence field is edited When the user inputs values that violate system-configured cadence rules (e.g., interval gaps, max reminders) Then an inline error describes the violated rule, the row status becomes "Needs Attention," and the invalid values are not committed Given the user corrects the cadence values to meet rules When the edit is committed Then the error clears, the row status returns to "Valid," and the impact summary updates accordingly
Bulk Selection, Copy/Paste, and Per-Column Apply
Given multiple community rows are selected When the user performs a per-column Bulk Apply (e.g., set Amount or Due Date) Then the value is applied to each selected row independently; rows that fail validation show inline errors and remain "Needs Attention" without blocking valid rows Given multiple rows are selected When the user pastes a single value into a column Then the pasted value is replicated to all selected cells with per-row validation and error reporting Given multiple rows are selected When the user pastes a range of values matching the selection shape Then values map row-by-row; each cell validates independently, successful cells commit, and failures are flagged inline
Row Status Updates and Real-Time Impact Summaries
Given the grid has pending or committed inline edits When any field edit is validated successfully Then the row status updates to "Valid" and the impact summary recalculates totals (e.g., total amount, rows affected, rows needing attention) in real time Given any field edit fails validation When the error is shown Then the row status updates to "Needs Attention" and the impact summary reflects the count of affected rows without including invalid values in totals Given multiple edits are made across rows When the user reviews the impact summary Then the summary accurately reflects the current state of all rows without requiring a page refresh
Billing Engine Constraint Enforcement
Given a community row is subject to billing engine constraints (e.g., existing invoices or compliance holds) When the user attempts to edit a constrained field (Amount, Due Date, Channel, Audience, or Cadence) Then the field shows a non-editable state or inline error indicating the specific engine constraint, and the row status becomes "Needs Attention" Given a bulk apply or paste operation includes constrained rows When the operation is executed Then unconstrained rows apply successfully, constrained rows are skipped with clear inline reasons, and no silent failures occur Given edits are committed When the system validates with the billing engine Then conflicting changes are rejected with structured error details referencing the constraint, and non-conflicting changes are persisted
Community Exemptions
"As a community manager, I want to exempt specific communities from a bulk update with a recorded reason so that local policies are respected and documented."
Description

Provide a per-community Exempt toggle to exclude communities from the current global action, requiring a reason and optional attachment (e.g., board directive). Exemptions must be clearly labeled, excluded from apply counts, and reversible. Persist exemptions within the override draft and reflect them in the audit trail and impact summary. Ensure downstream billing/reminder jobs skip exempted communities while preserving the original feed post for transparency.

Acceptance Criteria
Toggle Exemption Updates UI and Counts
Given an override draft listing communities When the user toggles Exempt ON for Community X Then Community X displays an "Exempt" badge in its row And the amount, date, channel, and audience fields for Community X become read-only And Community X is removed from the Apply target count and monetary totals within 1 second And the global Apply preview/tooltip reflects the updated counts
Reason Required; Attachment Optional with Validation
Given Exempt is ON for Community X When the user attempts to save the draft or navigate away Then a reason is required with 5–500 characters; HTML is stripped; leading/trailing whitespace is trimmed And an optional attachment may be uploaded in PDF, DOCX, PNG, or JPG format up to 10 MB And invalid inputs show inline error messages and prevent save And on successful save, the reason and attachment filename are displayed in Community X row
Exemption Persists in Draft Across Navigation and Reload
Given Community X is marked Exempt with a saved reason and optional attachment When the user reloads the page, switches tabs within the Override Panel, or closes and reopens the draft Then the exemption state, reason text, and attachment reference are restored without loss And the draft shows an updated Last saved timestamp within 5 seconds of the last change
Audit Trail and Impact Summary Include Exemptions
Given an override with one or more exemptions is saved or applied When viewing the override audit trail Then each exemption event records user identity, timestamp, community name/ID, reason text, and an attachment link (if provided) And the impact summary shows totals: total communities, exempted, applied, and skipped And audit exports (CSV/JSON) include the exemption fields and values
Downstream Jobs Skip Exemptions; Feed Post Preserved
Given an override with exemptions is applied When billing or reminder jobs are generated and executed Then no jobs are created or executed for exempted communities And the original feed post remains visible in those communities with a badge "Global action not applied (Exempt)" And job generation logs show zero tasks queued for each exempted community
Reversing an Exemption Re-includes Community
Given Community X is currently Exempt in an in-progress override draft When the user toggles Exempt OFF for Community X and saves Then Community X is re-included in Apply counts and monetary totals And its editable fields (amount, date, channel, audience) are re-enabled And the audit trail records a reversal event with user identity and timestamp; the prior reason/attachment remain referenced historically
All Communities Exempted State Handling
Given all communities in the override draft are set to Exempt When the user views the Apply panel Then the Apply target count displays 0 and the Apply action is disabled And a non-blocking note explains that all communities are exempted and nothing will be applied until at least one exemption is removed
Reusable Exception Rules
"As a part-time manager, I want to save common exceptions as reusable rules so that future overrides automatically apply the right local variations without rework."
Description

Allow saving per-community adjustments as named, reusable rules (e.g., "Small HOA Late Fee Cap", "Florida Statute 720 Notice Window"). Provide a rule engine to define conditions (community attributes, jurisdiction, delinquency status, portfolio tags) and outcomes (field overrides, channel restrictions, audience filters). Support rule precedence, scoping to action types (bills, announcements, reminders), versioning, and activation windows. When a new override is created, auto-suggest and auto-apply matching rules with a preview of their impact. Include a rules library with search, owners, change history, and the ability to export/import across portfolios.

Acceptance Criteria
Save Per-Community Adjustment as a Named Reusable Rule
- Given I have an approved per-community override, When I select "Save as Rule", Then I must provide Rule Name (unique within portfolio), Owner, optional Description, and optional Tags before saving. - Given valid inputs, When I save, Then the system persists the rule with a unique Rule ID, createdBy=current user, createdAt (UTC), and initial version=1, and responds with success. - Given a duplicate Rule Name in the same portfolio, When I attempt to save, Then validation prevents save and displays a clear error message. - Given the rule is saved, When I open the Rules Library, Then the rule is listed with status Active and is searchable by its name and tags.
Define Conditions and Outcomes in the Rule Engine
- Given the rule editor, When I add conditions, Then I can target community attributes, jurisdiction, delinquency status, and portfolio tags using operators (=, !=, in, contains, >=, <=) with type validation. - Given multiple conditions, When I group them, Then I can combine with AND/OR and nest up to 3 levels with a validated expression preview that is unambiguous. - Given outcomes are configured, When I define them, Then I can set field overrides (e.g., amount formula or cap, due date offset in days), channel restrictions (enable/disable channels), and audience filters (include/exclude segments) with schema validation. - Given a draft rule, When I click "Test Against Community" and input a community identifier, Then the engine returns Match/No Match and shows the computed outcomes without persisting changes.
Enforce Rule Precedence and Deterministic Conflict Resolution
- Given multiple active rules match the same action context, When outcomes conflict on the same field, Then precedence is applied in order: higher Priority (0–100), then greater predicate count, then newer effectiveStart, then lowest Rule ID; only the highest-precedence outcome applies to that field. - Given outcomes target different fields, When multiple rules match, Then the outcomes are merged without conflict. - Given at least one conflict occurs, When the preview is shown, Then a Conflict Resolution panel lists suppressed rules per field with their precedence rationale. - Given two rules are fully tied on precedence for the same field, When evaluation runs, Then auto-apply is blocked and the user must choose one before saving.
Scope Rules to Action Types (Bills, Announcements, Reminders)
- Given a rule scoped to Bills only, When I create an Announcement override, Then the rule is neither suggested nor applied. - Given a rule scoped to Bills and Reminders, When I create a Bill override, Then the rule is eligible for evaluation and application. - Given a rule scoped to All, When I create any override, Then the rule is eligible. - Given I edit a rule scope, When I save, Then the new scope is persisted and reflected immediately in subsequent evaluations.
Rule Versioning and Time-Bound Activation Windows
- Given a saved rule v1, When I edit and save changes, Then a new immutable version v2 is created and v1 remains read-only in history with timestamps and editor identity recorded. - Given a rule version has an Activation Window (start and optional end), When now is within the window and status is Active, Then the version participates in evaluation; when outside, it is excluded. - Given overlapping activation windows for versions of the same rule, When I attempt to save, Then the system blocks the save and prompts me to resolve the overlap. - Given I disable a rule or version, When I save, Then its status becomes Disabled and it is excluded from evaluation immediately and reflected in the library.
Auto-Suggest and Auto-Apply Matching Rules with Impact Preview
- Given I open the Override Panel to create a new override, When I select a community and action type, Then matching rules are evaluated and suggestions render within 500 ms for up to 100 active rules in the portfolio. - Given a suggested rule has Auto-Apply enabled and no precedence conflict, When suggestions load, Then the rule is applied automatically and labeled "Applied by Rule" with an Undo option. - Given suggestions are visible, When I expand a suggested rule, Then I see a diff of impacted fields (before/after) and a readable list of satisfied conditions. - Given a rule was applied (auto or manual), When I save the override, Then the override’s audit log records ruleId, ruleVersion, appliedBy user, and timestamp.
Rules Library: Search, Ownership, Change History, and Cross-Portfolio Export/Import
- Given the Rules Library, When I search by name, tag, owner, scope, status, or jurisdiction, Then results are correctly filtered and return within 300 ms for up to 500 rules. - Given I open a rule, When I view Change History, Then I see chronological entries with who changed what and when, including previous and new values and a required reason note for each change. - Given role permissions, When the user is Owner or Admin, Then they can edit and delete; otherwise access is read-only. - Given I export selected rules, When I choose JSON export, Then a file containing rule definitions, versions, and metadata is generated; When importing into another portfolio, Then validation reports conflicts and, upon confirmation, the rules are created with new IDs and owners mapped or flagged for assignment.
Immutable Audit Trail
"As a board secretary, I want a clear audit of who changed what and why so that our governance remains transparent and defensible."
Description

Capture a complete, immutable audit trail for every override: user identity, timestamp, global action details, per-field before/after values, exemption reasons, applied rules, validation results, and affected communities. Link audit entries to the originating feed post/bill and to any triggered reminders or payments. Provide row-level "why" context (manual edit vs. rule) and exportable reports for board review. Store signatures/hashes to make records tamper-evident, surface audit per row in the grid, and expose APIs for governance and compliance audits.

Acceptance Criteria
Global Override with Inline Edits Captures Complete Audit Record
Given an authenticated board admin opens the Override Panel And a global action exists When the admin edits per-community Amount, Due Date, Channel, and Audience inline for at least two communities and clicks Save Then the system creates one audit batch with a unique audit_id and a child entry per affected community And each child entry stores user_id, user_role, session_id, ip_address, user_agent, iso8601_timestamp, global_action_id, global_action_payload_snapshot, affected_community_id, and field_diffs [{field, before, after}] And validation_results per field are recorded with status (pass/fail) and messages And unchanged fields are excluded from field_diffs And audit entries are append-only (no update endpoint; any update attempt returns 403 and is logged as a tamper_attempt) And entries are available via the audit read API within 1 second of commit
Exemption with Justification and Saved Rule Is Fully Logged
Given an authenticated board admin views the Override Panel When the admin exempts a community from the global action and provides a justification note (min 10 chars) and optional attachment Then the audit entry for that community includes exemption_reason, attachment_ids, exempted_by, and iso8601_timestamp And the community is marked as exempt in the audit with field_diffs showing action_status: before=applied, after=exempt When the admin clicks “Save as reusable rule” and defines scope and criteria Then a rule record is created and the same audit entry includes rule_id, rule_name, rule_version, rule_scope, rule_expression, created_by, created_at And subsequent rows affected by this rule reference rule_id in their why context
Row-Level 'Why' Context Differentiates Manual vs Rule
Given a reusable rule exists that adjusts Amount by 10% When the override is applied and the rule changes a row value Then the row’s audit entry sets why_type=rule and includes rule_id, rule_name, rule_version, rule_evaluation_result When a user subsequently edits the same field manually Then a new audit entry is created with why_type=manual_edit and prior_entry_id referencing the rule-driven change And both entries appear in chronological order for that row with accurate before/after values And no audit entry is merged or overwritten
Audit Links to Originating Post/Bill and Downstream Events
Given a global override originates from a feed post or bill When the override is saved Then each audit entry includes post_id (or bill_id if originating from a bill) and affected_community_id And if reminders or payments are triggered, reminder_ids[] and payment_ids[] are populated with resolvable identifiers And the audit linkage endpoint returns 200 and resolves each id to an existing entity And if no downstream events exist, the corresponding arrays are present and empty
Tamper-Evident Hashes and Signature Verification
Given audit entries are created When the system commits each entry Then it computes a canonical JSON payload, a SHA-256 hash, and stores a detached digital signature and signer_id And the verification endpoint /audit/{id}/verify returns 200 with verified=true and the same hash for an unmodified record When any mutation to a stored audit payload is attempted Then the write is rejected with 409, no change to the payload occurs, and a tamper_attempt audit event is appended And exported records include hash and signature fields for external verification
Inline Audit Panel per Row in Grid
Given a user with Audit:View permission is on the Override Panel When the user clicks the Audit icon for a row Then a side panel opens within 500 ms (p95) for rows with up to 50 events And it displays chronological events with who (name, role), when (timestamp), why_type, and before/after diffs for changed fields And users without permission see the icon disabled and API access returns 403 And the panel supports paging or lazy-load for >50 events without blocking the UI thread
Export and Governance API Provide Complete, Filterable Output
Given an auditor role requests an export for a date range, community set, and action types When requesting CSV and JSON exports Then the file includes all required columns: audit_id, timestamp, user_id, action identifiers, field_diffs, why_type, rule references, validation results, linkages, hash, signature And exports of up to 100k entries complete within 60 seconds and are downloadable with a checksum When querying the governance API Then GET /audit supports pagination (page, page_size), sorting, and filters (date, user_id, community_id, action_type, why_type, rule_id) And responses return 200 with total_count and schema-compliant fields; unauthorized requests return 401/403; rate limits return 429 with retry-after
Role-Based Override Permissions
"As an account owner, I want granular control over who can draft and apply overrides so that risk is minimized and policy is enforced."
Description

Enforce granular permissions for creating, editing, approving, and applying overrides, including rights to save rules, exempt communities, and change channels/audiences. Support optional two-person approval for high-impact actions, SSO group mapping, and per-community ACLs. Display permission-driven UI states (read-only cells, disabled actions) and provide clear, non-technical denial reasons. Log all access checks and approvals in the audit trail. Ensure background jobs honor the same permissions when applying scheduled overrides.

Acceptance Criteria
Granular Override Action Permissions
Given a user with no override permissions, when they open the Override Panel, then all cells are read-only and Create/Apply/Save Rule/Exempt actions are disabled or hidden. Given a user with Override.Create, when they create a new override draft, then the draft is created and an ALLOW audit entry is recorded. Given a user lacking Override.Edit for a community, when they attempt to edit amounts/dates/channels/audiences inline, then the edit is blocked and a clear denial message is shown and logged. Given a user with Override.Exempt for a community, when they toggle Exempt Community for that community, then the exemption saves only for that community and is reflected in the grid. Given a user with Override.SaveRule, when they save an exception as a reusable rule, then the rule is created and appears in the rule library with their user recorded as creator. Given a user lacking Override.ChangeChannels or Override.ChangeAudience, when they attempt to change those fields, then controls are disabled and any direct API call returns a denial with reason code and user-friendly message.
Two-Person Approval for High-Impact Actions
Given high-impact approval is enabled, when a user with Override.Approve submits an override that meets the high-impact threshold, then its status becomes Pending Approval (0/2). When the same user attempts to approve their own submission, then the system rejects the approval with message requiring a different second approver and logs the attempt. Given two distinct users each with Override.Approve and community access, when both approve within the policy window, then status becomes Approved (2/2) and the override is eligible to apply. Given one approver denies, then the override status becomes Rejected and Apply is disabled. Given the approval window expires before second approval, then the override reverts to Draft and cannot be applied. All approval/denial actions capture who, when, community scope, comments, and decision in the audit trail.
SSO Group-to-Role Mapping
Given an SSO assertion includes groups G1 and G2, when group-to-role mappings exist, then the user is assigned the corresponding internal roles on sign-in and receives the associated override permissions. Given a mapping is changed by an admin, when the user signs in again or their session refreshes, then effective permissions reflect the new mapping without manual user edits. Given no mapping grants the required permission, when the user attempts a restricted override action, then access is denied with a non-technical message and the denial is logged with reason No mapped role. Given a user is removed from an SSO group mapped to override permissions, when they next perform an authorization check, then access is denied for those actions and reflected in UI state.
Per-Community Access Control Lists (ACLs)
Given a user has override permissions for Community A but not Community B, when viewing the side-by-side grid, then A is editable per rights and B is read-only with disabled actions. When the user applies an override to multiple communities, then the system applies to communities where the user has Override.Apply and reports per-community success/denial results. Given a user with global admin role, when they perform override actions, then actions are permitted across all communities per policy and logged accordingly. Given a user lacks Override.Exempt for a community, when they attempt to exempt that community, then the action is blocked with a clear message and an audit DENY event.
Permission-Driven UI States and Denial Messaging
Controls for which the user lacks permission are disabled or hidden consistently across grid, detail modals, and bulk actions. Read-only cells display a lock icon and a tooltip that explains in non-technical language which permission is missing and for which community. Triggering a disabled action via keyboard or API produces no state change and surfaces a banner/toast with the same non-technical denial reason; screen readers announce the disabled state. Denial messages avoid codes/jargon, name the action and scope, provide next step (e.g., contact an admin), and are localized to the user’s language.
Background Jobs Honor Permissions on Scheduled Overrides
Given a scheduled override has the required approvals, when the background job runs, then it validates approvals are present and not expired before applying. The job applies overrides only to communities where the approver-of-record or configured service role has Override.Apply and the community ACL permits it; other communities are skipped with a logged reason. If permissions or ACLs change between approval and execution, then the job re-evaluates and denies as appropriate, emitting per-community audit entries. The job never escalates privileges; missing permissions result in a no-op for those targets and a Skipped — permission denied outcome visible in run results.
Audit Trail for Access Checks and Approvals
Every allow/deny decision for override actions (create, edit, approve, apply, save rule, exempt, change channel/audience) writes an audit event with requestId, userId, action, communityId(s), targetId, decision (ALLOW/DENY), reason, and timestamp. Approval flows record both approvers, comments, and status transitions with timestamps. Audit events are queryable by date range, user, community, and action, and appear in the audit UI within an acceptable delay. Background job executions append per-community outcomes with the same schema correlated to the job run id. Audit entries contain no sensitive payment data; PII fields are redacted per platform policy.
Preview, Schedule, and Rollback
"As a portfolio lead, I want to preview and schedule overrides with a safe rollback option so that I can deploy changes confidently without disrupting operations."
Description

Offer a pre-apply impact preview summarizing affected communities, totals, deltas from current settings, and any conflicts with existing bills or reminders. Allow scheduling the apply time with timezone support, and provide a dry-run mode to surface validation issues in advance. On apply, execute in batches with retry/backoff and partial-failure reporting. Provide one-click rollback to the last known good state with a clear diff and automatic reversion of related reminders. Notify stakeholders on schedule, apply, failure, or rollback events via chosen channels.

Acceptance Criteria
Impact Preview Summarizes Affected Communities and Conflicts
Given a global action with per-community overrides is configured And at least one community has existing bills or reminders that may conflict When the user opens the Impact Preview Then the preview lists all affected communities with per-community amount/date/channel/audience deltas from current settings And totals (number of communities and total amount) are displayed and computed correctly And conflicts with existing bills or reminders are flagged with type and reason per community And exempted communities are explicitly marked and excluded from totals And updating any override inline updates the preview and recalculated totals without page reload
Timezone-Aware Scheduling of Apply
Given the user selects a schedule date/time and a timezone When the schedule is saved Then the system stores the execution time as a UTC instant equivalent to the selected local time And displays the scheduled time localized to the viewer's timezone And prevents scheduling in the past by showing a validation error And if the selected local time is invalid or ambiguous due to DST, the user is prompted to resolve the ambiguity before saving
Dry-Run Validation Surfaces Issues Without Side Effects
Given a configured global action When the user runs a Dry Run Then no bills, reminders, or settings are created, modified, or deleted And the system returns a per-community validation report with severity (error/warning/info), message, and suggested fix where applicable And if errors are present, the Apply action requires explicit confirmation with rationale from an authorized role before proceeding And Dry Run results are timestamped and clearly labeled as simulated
Batched Apply with Retry, Backoff, and Partial-Failure Reporting
Given the batch size is configured to 100 items When the user clicks Apply Now Then the system processes items in batches not exceeding the configured size in a deterministic order And transient failures are retried with exponential backoff up to 3 attempts per item And a run report shows per-batch and per-community status (success/retried/failed), error messages, and counts of successes and failures And the operation is idempotent so re-running Apply does not create duplicate bills or reminders
One-Click Rollback to Last Known Good State with Diff
Given an apply run has completed and a last known good checkpoint was created When the user clicks Rollback for that run and confirms Then the system reverts all changes made by the run to the last known good state, including automatic cancellation or restoration of related reminders And a diff of fields and counts to be reverted is displayed before confirmation and an outcome report after execution And partial failures are reported with retry options, while successful reversions remain intact And Rollback is available only for the most recent run affecting that scope
Stakeholder Notifications on Schedule, Apply, Failure, and Rollback
Given stakeholders and channels (email and in-app at minimum) are selected for a run When a run is scheduled Then stakeholders receive a schedule notification containing actor, scheduled time with timezone, summary of affected communities, and a link to the preview And when apply starts and completes, stakeholders receive start and completion notifications with success/failure counts and a link to the run report And on failure or rollback, stakeholders receive failure/rollback notifications with error summaries and links to details And notifications honor per-user channel preferences
Audit Trail for Dry Run, Schedule, Apply, and Rollback
Given a dry run, schedule, apply, or rollback action is initiated When the action is submitted Then an immutable audit entry is created recording actor (who), rationale (why), timestamp, parameters (scope and overrides), preview summary, validation results, and outcome (success/failure with errors) And audit entries are filterable by action type and date and are viewable alongside the run report

Wave Scheduler

Roll out sends and bills in timed waves that respect time zones and quiet hours. Rate‑limit to avoid support spikes, auto‑pause on bounce/error thresholds, and auto‑retry failed deliveries. Try A/B variants on the first wave and expand the winner automatically to the rest.

Requirements

Time Zone-Aware Scheduling
"As a board admin, I want messages and bills to send at the same local time for each recipient so that homeowners receive them at reasonable, predictable hours."
Description

Schedule announcements and bill posts so each recipient receives them at an equivalent local time based on their time zone. Resolve time zone via community settings, homeowner profile, and property address geolocation with safe fallbacks. Handle daylight saving time shifts automatically, provide a preflight schedule preview, and log the chosen time zone and computed send time per recipient. Applies consistently across email and in-app notifications.

Acceptance Criteria
Resolve Recipient Time Zone with Fallbacks
- Given a recipient has a valid IANA time zone on their homeowner profile, when a post is scheduled for "9:00 AM local time", then the system resolves their time zone from the profile and schedules the send at 09:00 in that zone. - Given the profile time zone is missing or invalid, when scheduling, then the system uses the community default time zone if valid. - Given both profile and community time zones are missing or invalid, when scheduling, then the system geocodes the property address and maps it to an IANA time zone, and uses that zone if successful. - Given profile, community, and geolocation are unavailable or invalid, when scheduling, then the system assigns the configured platformDefaultTimeZone and continues. - Then 100% of targeted recipients have a resolved IANA time zone and a computed UTC send timestamp before the job is enqueued. - And the resolutionSource (profile|community|geolocation|platformDefault) is stored per recipient.
Local‑Time Sends Honor DST Transitions
- Given a campaign is scheduled for 02:30 local time on a spring‑forward date where 02:00–02:59 do not exist, when computing send times, then the send is scheduled at the next valid local time (03:00) in the recipient’s zone. - Given a campaign is scheduled for 01:30 local time on a fall‑back date where 01:00–01:59 occurs twice, when computing send times, then the send is scheduled at the first occurrence (earlier UTC) of 01:30 in the recipient’s zone. - Given a campaign is scheduled for 09:00 local time in zones without DST, when computing send times, then the UTC timestamp is derived from the fixed offset for that date and zone. - Then all computed UTC send timestamps are derived using the current IANA time zone database for the target date (not fixed offsets), and unit tests cover at least three DST‑observing zones and two non‑DST zones.
Preflight Schedule Preview by Recipient Time Zone
- Given an author selects a local send time (e.g., 09:00) and an audience, when opening the preflight preview, then the UI displays for each distinct recipient time zone: the local send time, the corresponding UTC timestamp for the scheduled date, and the count of recipients in that zone. - And the preview displays up to 5 sample recipients per time zone showing name and destination channel(s). - And the sum of counts across time zones equals the total audience size, with zero recipients omitted. - And recipients resolved via platformDefaultTimeZone are flagged with a warning badge and total count. - Then the author can confirm scheduling only after the preview has successfully loaded with no unresolved time zones.
Per‑Recipient Time Zone and Send‑Time Audit Logging
- Given a post is scheduled, when the send job is created, then for each recipient the system logs: postId, recipientId, resolved IANA time zone, resolutionSource, and computed UTC send timestamp with createdAt. - And the log record is immutable and queryable by postId and recipientId. - Then 100% of recipients in the job have exactly one corresponding log record before dispatch begins.
Channel Consistency: Email and In‑App
- Given a recipient has both email and in‑app notifications enabled, when the post is scheduled, then both channels use the same computed UTC send timestamp derived from the recipient’s resolved time zone. - And actual delivery initiation for both channels occurs within ±60 seconds of the scheduled UTC timestamp. - Given a recipient has only one channel enabled, when scheduling, then only that channel is scheduled, and no schedule is created for the disabled channel.
Graceful Handling of Missing/Invalid Inputs
- Given a recipient is missing a profile time zone and has no mappable property address, when scheduling, then the system assigns platformDefaultTimeZone and includes the recipient in the job. - Given any time zone string provided by profile or community is not a valid IANA identifier, when scheduling, then the system treats it as invalid and proceeds to the next fallback source without failing the job. - Given any recipients rely on platformDefaultTimeZone, when loading the preflight preview, then a warning banner lists the count and allows the author to proceed without blocking. - Then no recipient is dropped from scheduling solely due to time zone resolution issues.
Quiet Hours Enforcement
"As a board admin, I want to prevent sends during quiet hours so that we respect residents’ preferences and reduce complaints."
Description

Enforce community-defined and optional per-recipient quiet hours to prevent deliveries during restricted periods. Automatically defer any send falling within quiet hours to the next permissible delivery window while preserving wave order. Support channel-specific rules (e.g., email allowed earlier than SMS), weekend preferences, and urgent-override options with explicit confirmation. Provide conflict detection, a visual preview of deferrals, and detailed logs of rescheduling decisions.

Acceptance Criteria
Deferral to Next Permissible Window Respects Time Zones and Wave Order
Given community quiet hours are 20:00–08:00 local And wave 1 is scheduled for 07:30 local and wave 2 is scheduled for 07:45 local And recipients are in multiple time zones (e.g., America/New_York, America/Denver) When the waves execute Then for each recipient, wave 1 is deferred to 08:00 recipient local time And wave 2 for that recipient is deferred to 08:15 recipient local time (preserving the original 15‑minute gap) And no wave 2 delivery occurs for any recipient before that recipient’s wave 1 delivery
Per-Recipient Quiet Hours Override
Given community default quiet hours are 21:00–07:00 local And recipient R has custom quiet hours 19:00–09:00 local And a send is scheduled for 08:30 recipient local time When scheduling is computed Then recipient R’s send is deferred to 09:00 local (custom rules take precedence) And recipients without custom quiet hours deliver at 08:30 local And a send at exactly 19:00 for R is deferred, while a send at exactly 09:00 is allowed
Channel-Specific Quiet Hours Enforcement
Given channel quiet hours are Email: 22:00–06:00, SMS: 20:00–09:00, Push: none And a wave is scheduled for 07:30 recipient local time on all channels When the wave executes Then Email delivers at 07:30 And SMS is deferred to 09:00 And Push delivers at 07:30 And no channel is delivered inside its own quiet hours window
Weekend Preference Handling
Given weekend preference is “No weekend delivery” And quiet hours are 21:00–07:00 local And wave 1 is scheduled for Saturday 08:00 recipient local time and wave 2 is scheduled for Saturday 08:30 When the schedule is computed Then wave 1 is deferred to Monday 08:00 local And wave 2 is deferred to Monday 08:30 local (preserving original gap and order) And subsequent waves are shifted to maintain original inter‑wave gaps and order for each recipient
Urgent Override Requires Explicit Confirmation and Audit
Given a send falls within quiet hours for selected recipients/channels And the actor enables Urgent Override, types “SEND NOW” to confirm, and provides a reason of at least 10 characters When the send is confirmed Then the selected recipients/channels bypass quiet hours and deliver immediately And the system records an audit entry with actor, timestamp (UTC), reason, override scope, and affected count And if either the confirmation phrase or reason is missing, the send is blocked and no messages are sent
Conflict Detection and Visual Deferral Preview
Given the configured rules produce no permissible window within the next 14 days for some recipients When preparing the wave in the scheduler Then the system displays a conflict banner showing the count and percentage of affected recipients per channel And the preview panel shows predicted delivery timestamps, time zones, and deferral reasons for at least 10 sample recipients per channel And the “Schedule” action is disabled until the user adjusts rules, removes affected recipients, or applies Urgent Override
Rescheduling Decision Logging
Given any send is allowed, deferred, or overridden due to quiet hours logic When the send is processed Then a log entry is created per message containing original scheduled time (UTC), recipient local time zone (IANA), policy source (community|recipient|channel), weekend rule applied, decision (allowed|deferred|overridden), final send time (UTC), delay duration, wave number, and reason code And logs can be filtered by date range, wave, channel, decision, and policy source And logs can be exported to CSV including all listed fields
Configurable Wave Builder & Rate Limiting
"As a part-time manager, I want to roll out sends in controllable waves with rate limits so that I can monitor response and avoid overwhelming support."
Description

Allow admins to construct delivery waves by percentage, fixed count, or audience segments, and schedule intervals between waves. Apply global and per-community rate limits (e.g., messages per minute) with dynamic throttling to smooth spikes and protect support capacity. Include dry-run previews, capacity checks, and editable parameters until a wave is locked. Support both announcements and bill posts, ensuring idempotent execution and consistent sequencing across channels.

Acceptance Criteria
Wave Construction by Percentage, Count, and Segments
Given an audience of N recipients, when an admin defines waves by percentage, then the system calculates integer counts per wave that sum to at most N and displays the counts before save. Given an audience of N recipients, when an admin defines waves by fixed counts, then save succeeds only if the sum of counts is less than or equal to N; otherwise save is blocked with a clear validation error. Given overlapping audience segments, when waves are defined by segment and/or combined with percentage/count waves, then each recipient is included in at most one wave via deterministic deduplication. Given waves whose total coverage is less than N, when saving, then the UI offers to add a remainder wave; when accepted, the remainder wave is added; when declined, the final coverage percentage is displayed. Given a saved schedule, when previewing composition, then counts per wave and a reproducible sample of recipients per wave are shown.
Inter-Wave Scheduling and Intervals
Given K waves and an interval I minutes, when the schedule is started at T0, then wave i's earliest dispatch time is T0 + (i−1)*I and waves are queued accordingly. Given edits to interval I prior to lock, when saving, then subsequent wave start times update accordingly in the preview. Given wave i completes later than planned, when scheduling wave i+1, then wave i+1 does not start before wave i completes and at least I minutes have elapsed since wave i started.
Global and Per-Community Rate Limiting with Dynamic Throttling
Given a global rate limit G messages/min and per-community limit Cx for community X, when dispatching, then in any rolling 60-second window total dispatched messages across all communities is less than or equal to G and for community X is less than or equal to Cx. Given instantaneous demand that would exceed any limit, when dispatching, then the system enqueues excess and releases messages smoothly such that no rolling 60-second window exceeds G or any Cx. Given configured throttling triggers (e.g., backlog_threshold, error_rate_threshold, support_queue_threshold) and a throttle_reduction_percent R, when any trigger is met, then the dispatch rate is reduced by at least R% within 60 seconds and remains at or below the reduced rate until triggers clear for the configured cooldown period; when triggers clear for the cooldown, the rate ramps back up without exceeding limits. Given missing configuration for G or any required Cx, when saving a schedule, then validation fails with required-field errors until limits are set.
Dry-Run Preview and Capacity Checks
Given a configured schedule and rate limits, when running a dry-run, then no messages or bills are sent/created and no external integrations are invoked. Given a dry-run, when rendering results, then the system displays per-wave recipient counts, estimated start and end timestamps per wave, total estimated duration, and the effective rate used (minimum of applicable global and per-community limits). Given the estimated plan, when any wave's estimated start time would be delayed due to rate limits, then the preview shows adjusted times and flags the delay. Given the schedule exceeds a configured maximum duration or daily capacity cap, when generating the dry-run, then the system blocks locking and surfaces a clear capacity breach message.
Editable Parameters and Locking Behavior
Given an unlocked schedule, when editing wave composition (mode, percentages/counts/segments), intervals, or rate limits, then changes can be saved and are reflected in the preview. Given a schedule is locked, when attempting to edit wave composition, intervals, or rate limits via UI or API, then the system rejects the change with HTTP 409 Conflict and error code "ScheduleLocked", and no changes are persisted. Given a schedule transitions to dispatching, then the system sets status to Locked before sending any messages. Given any change (pre-lock), when saved, then an audit log entry is recorded with actor, timestamp, and before/after values.
Support for Announcements and Bill Posts with Idempotent Execution
Given a post of type Announcement, when scheduled, then deliveries are created and sent per wave without creating any billing artifacts. Given a post of type Bill, when scheduled, then one invoice/bill artifact per recipient is created at first successful dispatch and no duplicate artifacts are created on retries or re-runs. Given a retry of a previously attempted delivery (same schedule, same recipient, same channel), when dispatching, then idempotency keys prevent duplicate sends and charges. Given a manual re-run of a locked schedule, when executed, then no duplicate sends or bills occur; only previously failed deliveries are retried.
Consistent Sequencing Across Channels
Given a single schedule delivering via in-app, email, and SMS, when dispatching wave i, then the same recipient ordering is applied across channels and channels do not deliver any wave j > i content to a recipient before that recipient has received wave i content. Given a configured cross-channel skew threshold S, when dispatching, then for any recipient and wave, the time difference between first and last channel delivery is less than or equal to S. Given delivery logs, when auditing, then per-recipient, per-channel sequence numbers and timestamps are recorded to verify ordering and skew. Given rate limiting is active, when dispatching across channels, then sequencing guarantees above still hold.
Delivery Health Auto-Pause & Alerts
"As a board admin, I want campaigns to auto-pause on delivery problems so that we protect our sender reputation and prevent resident frustration."
Description

Continuously monitor delivery health signals (bounce, complaint, hard-fail, unsubscribe rates) per wave and channel. When configurable thresholds are exceeded, automatically pause the campaign, prevent subsequent waves from starting, and notify admins via in-app and email alerts. Provide clear diagnostics, remediation guidance, and safe resume controls. Persist pause state across system restarts and allow threshold templates with community-level overrides.

Acceptance Criteria
Auto-Pause on Breach of Delivery Health Thresholds
Given an active campaign wave is sending on a specific channel with thresholds configured for bounce_rate, complaint_rate, hard_fail_rate, and unsubscribe_rate When any one metric for that wave and channel equals or exceeds its configured threshold Then the system auto-pauses the entire campaign within 60 seconds, marks the current wave status as Paused, and halts all further deliveries and retries for that campaign across all channels And then an immutable audit log entry is recorded with campaign ID, wave ID, channel, breached metric, threshold, observed value, evaluation window, timestamp, and pausing actor = system And then the pause reason and breached metrics become visible in the campaign UI within 60 seconds And then zero additional messages are sent after the pause timestamp until an authorized admin resumes
Prevent Subsequent Waves After Pause
Given a campaign is in Paused state due to a threshold breach When the scheduled start time of any subsequent wave is reached Then the scheduler does not start those waves, marks them Blocked by Pause, and records a system log entry per wave And when a user attempts to manually trigger a blocked wave Then the UI prevents the action, displays the pause reason with a link to diagnostics, and no deliveries are initiated And then zero deliveries occur for any blocked wave until the campaign is resumed
Admin Alerts on Pause
Given at least one admin of the community has alert permissions enabled When a campaign auto-pauses due to a threshold breach Then an in-app notification is sent to all admins within 30 seconds and an email alert is delivered within 2 minutes And then each alert includes campaign name, community, wave, channel, breached metric, observed value vs threshold, pause timestamp, and a deep link to diagnostics And then alerts are de-duplicated so that no more than one pause alert per campaign-channel is sent within any rolling 15-minute window And then alert delivery outcomes (success/failure) are recorded with message IDs and timestamps in the audit log
Diagnostics and Remediation Guidance
Given a campaign is paused due to a delivery health breach When an admin opens the campaign’s diagnostics panel Then it displays, for the affected wave and channel: attempted, delivered, bounces, complaints, hard fails, unsubscribes, and their rates; a histogram of error codes/reasons; a time-series of the breached metric; and the last 100 failure samples with provider codes and timestamps And then the panel provides at least three contextual remediation suggestions (e.g., verify sender/domain, clean list segment, reduce rate limit), each with actionable links where applicable And then all displayed metrics and samples match backend telemetry within ±1% and are time-stamped And then the diagnostics view loads in under 2 seconds p95 for datasets under 50k events
Safe Resume Controls
Given a campaign is paused When an admin with Resume permission initiates Resume Then a confirmation modal summarizes the breach (metric, threshold, observed value), latest rates, and the effective thresholds, and requires explicit acknowledgement And then a pre-flight check runs (provider API reachability, sender/domain status, remaining audience size for the wave) and blocks resume on failures with actionable messages And then the admin can choose to resume the current wave only or skip current and proceed to the next scheduled wave And then upon resume, the system rate-limits the first 10 minutes to no more than 10% of the campaign’s configured max send rate and ramps to full rate thereafter unless overridden by the admin And then if the breached metric re-occurs above threshold during ramp-up, the system re-pauses within 60 seconds and records a new audit event
Pause State Persistence Across Restarts
Given a campaign is paused When the system or worker processes restart for any reason Then the campaign remains in Paused state upon recovery, and no deliveries, retries, or wave starts occur until an admin resumes And then the pause state, reason, timestamps, and breached metrics persist in durable storage and are reloaded within 30 seconds of service availability And then no duplicate pause alerts are sent solely due to the restart; only state changes trigger new alerts
Threshold Templates with Community Overrides
Given threshold templates exist for bounce_rate, complaint_rate, hard_fail_rate, and unsubscribe_rate per channel When a new campaign is created for a community Then the community’s assigned template applies by default and the UI shows the effective thresholds per metric/channel and their source (template vs default) And when the community defines an override for any metric/channel Then the effective threshold for that campaign reflects the override immediately for waves not yet started, and the UI indicates the override And then changes to templates or overrides are audit-logged with who, what, before/after, and timestamp And then delivery health evaluation and auto-pause use the effective thresholds exactly as displayed
Automatic Retry with Exponential Backoff
"As a treasurer, I want failed deliveries to retry automatically without duplicate billing so that more residents receive their invoices without manual follow-up."
Description

Automatically retry transient delivery failures (e.g., soft bounces, timeouts) using exponential backoff with jitter and a maximum attempts cap. Respect quiet hours and rate limits during retries, with channel-specific policies. Track per-recipient retry history and final disposition. For bill posts, ensure idempotency and guard against duplicate charges or duplicate payment links in follow-up messages.

Acceptance Criteria
Exponential Backoff with Jitter for Transient Failures
Given an email delivery attempt returns a transient failure (soft bounce/timeout) and the email retry policy is configured with initialDelay=5m, backoffFactor=2, jitter=±20%, maxBackoff=6h, and maxAttempts=5 When the system schedules retries for this recipient Then attempt 2 is scheduled between 4m and 6m after the failure And attempt 3 is scheduled between 8m and 12m after attempt 2 And attempt 4 is scheduled between 16m and 24m after attempt 3, capped by maxBackoff if applicable And no attempt beyond attempt 5 is scheduled
No Retries on Permanent Failures
Given a delivery attempt fails with a permanent error (e.g., hard bounce, non-retryable code, invalid address/number) When the system evaluates the failure Then no retries are scheduled for this recipient on this channel And the final disposition is set to "Bounced - Permanent" with the provider error code And the recipient is excluded from subsequent retry waves for this message
Quiet Hours and Local Time Respect in Retry Scheduling
Given quiet hours are configured from 21:00 to 08:00 in the recipient’s local time zone And the recipient’s time zone is America/Los_Angeles And a transient failure occurs at 20:59 local time with a computed next delay of 5 minutes When the system schedules the next retry Then the retry does not occur during quiet hours And the retry is scheduled at the earliest allowed time after quiet hours (08:00 local) that is not earlier than the computed delay
Rate-Limited Retry Throttling
Given an organization-level rate limit of 300 email sends per minute and 50 SMS per minute And 1,000 recipients become eligible for retry in the same minute for each channel When the system enqueues retries Then actual sends per channel do not exceed the configured per-minute limits And excess retries are deferred to subsequent minutes in FIFO order within each wave and channel And scheduling logs record throttling decisions with counts per interval
Channel-Specific Retry Policies Enforced
Given the email policy is initialDelay=5m, maxAttempts=5 and the SMS policy is initialDelay=2m, maxAttempts=3 And Recipient A fails via email with a transient error And Recipient B fails via SMS with a transient error When retries are scheduled Then A’s retries follow the email policy timings and maxAttempts And B’s retries follow the SMS policy timings and maxAttempts And changing one channel’s policy does not alter the other channel’s scheduled retries
Per-Recipient Retry History and Final Disposition Tracking
Given a recipient experiences three transient failures followed by a successful delivery on the fourth attempt When viewing the recipient’s delivery log via UI and API Then the log lists each attempt with timestamp, channel, provider response/error code, computed delay, and jitter applied And the attempt count equals 4 and is within the configured maxAttempts And the final disposition is "Delivered" on attempt 4 And the log includes message ID, wave ID, scheduler node, and idempotency key
Bill Post Idempotency and Duplicate Protection on Retries
Given a bill post includes a payment link and an idempotency key for charge creation And the initial delivery times out and retries are sent When the recipient pays using any of the messages Then only a single charge is created on the backend for that idempotency key within a 48-hour window And all follow-up messages reference the same payment link URL and invoice ID (no duplicate links) And any duplicate charge or payment-link creation attempts return an idempotent response and do not create additional records And the recipient’s communications do not include multiple distinct payment links for the same bill
A/B Testing with Auto-Winner Expansion
"As a board admin, I want to test message variants on the first wave and auto-send the winner to the rest so that we maximize read rates and on-time dues."
Description

Enable two-variant tests within the first wave for subject/heading, message copy, and optional send-time variants. Randomly assign or segment recipients, measure key metrics (open rate, click-through to portal, and payment conversion within a configurable window), and select a winner using a configurable KPI and confidence threshold. Automatically apply the winning variant to the remaining waves, with reporting and manual override if needed.

Acceptance Criteria
Configure and Assign Two Variants in Wave 1
Given a first-wave send with Variant A and Variant B configured for subject/heading, message copy, and an optional send-time And a targeting mode is selected (random split with a chosen ratio or a defined segment rule) When the wave is launched Then each eligible recipient is deterministically assigned to exactly one variant according to the selected mode And for random split, the realized allocation is within ±2% of the requested ratio for cohorts of 1,000+ recipients, and within ±1 recipient for smaller cohorts And for segmented mode, 0 recipients cross segments and 0 recipients receive multiple variants And the assigned variant and planned send-time are stored per recipient and visible in delivery logs
Event Tracking and Metric Attribution per Variant
Given tracking is enabled for opens, portal clicks, and payments And a conversion window is configured starting at each recipient’s variant send-time When recipients open, click through to the portal, or complete a payment during the window Then the system records unique opens, unique click-throughs, and payment conversions per recipient per variant And metrics roll up per variant and per wave and are queryable in the reporting UI and API And payment conversions are attributed to the variant that generated the click or, if no click occurred, to the variant that was sent (view-through) And events outside the conversion window are excluded from conversion rate calculations
Winner Selection Using Configurable KPI and Confidence Threshold
Given a KPI is selected (open rate, click-through rate, or payment conversion rate) And a statistical confidence threshold is configured And an evaluation time is reached (scheduled or when a minimum sample size is met) When the system evaluates the variants Then it computes the KPI for each variant using unique recipient denominators And performs a two-tailed proportion significance test between variants And if one variant beats the other at or above the configured confidence threshold, that variant is marked as the winner And if no winner meets the threshold, the system retains both variants without auto-expansion and flags the test as inconclusive
Auto-Expansion of Winning Variant to Remaining Waves
Given a winning variant is determined for Wave 1 And there are remaining scheduled waves within the campaign When auto-expansion is enabled Then the system updates the remaining waves to send only the winning variant And previously scheduled content and send-times are replaced by the winning variant’s content and, if configured, send-time And the change respects each wave’s time-zone rules, quiet hours, and rate limits And no recipient who already received a first-wave message is re-sent due to the expansion
Manual Override of Winner and Expansion Behavior
Given an authorized user with the proper role accesses the test results When the user selects a different variant or disables auto-expansion before the next wave starts Then the system applies the override to all remaining waves And records the override user, timestamp, reason, and previous decision in the audit log And the reporting clearly labels the outcome as “Manually Overridden”
Reporting and Export of A/B Test Results
Given the A/B test has completed its first wave When viewing the reporting UI or exporting CSV Then the report includes for each variant: sent count, deliverability rate, unique open rate, unique click-through rate, payment conversion rate within the configured window, sample size, confidence, decision (winner/inconclusive), and decision time And the export contains recipient-level assignments and key events with timestamps and variant labels And all timestamps are in the campaign’s time zone and include offset
Audit Trail & Wave Reporting
"As a compliance officer, I want a complete audit trail and reports for each campaign so that we can verify delivery decisions and meet record-keeping requirements."
Description

Capture an immutable audit log of scheduling decisions, time zone resolution, quiet-hours deferrals, rate limiting actions, pauses, retries, and variant assignments. Provide wave- and campaign-level dashboards with KPIs, recipient-level outcomes, and export options (CSV/JSON). Link each Duesly post or bill to its delivery timeline for end-to-end traceability, with role-based access controls and retention policies to meet compliance requirements.

Acceptance Criteria
Immutable Audit Log Coverage & Integrity
Given any scheduling-related action (create/modify/cancel wave or campaign), When the action is executed, Then an audit event is appended with event_type, actor_id, actor_role, campaign_id, wave_id, post_or_bill_id, timestamp (UTC ISO-8601 ms), source_ip, and action-specific payload fields. Given the audit store, When querying events, Then events are strictly append-only with monotonically increasing sequence_id and no update/delete APIs exist; mutation attempts return 405 and are logged as security events. Given an audit event, When validating integrity, Then event_hash and previous_event_hash produce a verifiable tamper-evident chain for 100% of sampled events. Given client-supplied timestamps, When stored, Then server timestamps are authoritative and client timestamps are stored separately with client_time=true metadata.
Time Zone Resolution & Quiet Hours Deferrals Logged
Given a recipient requires time zone resolution, When a schedule is computed, Then an audit event records tz_source (profile/ip/default), tz (IANA), computed local time, and decision rationale. Given quiet hours are configured, When a send falls within quiet hours, Then a deferral event records reason=quiet_hours, original_scheduled_at, next_attempt_at, and quiet_hours_window. Given DST transitions, When scheduling across ambiguous times, Then the audit event records DST rule applied and the unambiguous UTC timestamp used. Given a fallback to default time zone, When resolution fails, Then the event records tz_source=default and the configured default_tz value.
Rate Limiting, Pauses, Retries, and Bounce Thresholds Logged
Given rate limiting is enabled, When a wave would exceed the allowed rate, Then a throttle event records throttle_applied=true, allowed_rate, current_rate, queued_count, and release_schedule. Given bounce/error thresholds are configured, When the rolling threshold is met, Then a pause event records pause_reason (bounce_threshold|error_threshold), scope (wave|campaign), threshold_config, and pause_started_at; dashboard state updates within 60 seconds. Given a delivery failure, When an auto-retry is scheduled or executed, Then retry events record retry_number, backoff_strategy, scheduled_at, attempted_at, provider_response, and outcome (success|failure|deferred) until max_retries is reached. Given a manual resume, When triggered by an authorized user, Then a resume event records actor_id, reason, and scope; subsequent sends resume respecting rate limits.
Wave & Campaign Dashboards with KPIs
Given a campaign or wave is selected, When viewing the dashboard, Then KPIs display counts for scheduled, sent, delivered, bounced, deferred, paused, retried, and rates for open, click, read, on_time_dues, and payment_completion over the selected time range. Given filters by wave, variant, recipient segment, and time range, When applied, Then metrics and charts update within 2 seconds for datasets up to 100k recipients and within 10 seconds up to 1M recipients. Given an A/B first wave, When a winner is auto-expanded, Then the dashboard marks the winning variant with confidence, rollout percentage, and rollout start time, and suppresses the losing variant for subsequent waves. Given any KPI tile, When clicked, Then the user can drill down to a paginated recipient list matching the KPI with server-side sorting and filtering.
Recipient-Level Outcomes & Delivery Timeline Linking
Given any Duesly post or bill, When opening its details, Then a Delivery Timeline tab shows ordered events (scheduled, tz_resolved, quiet_hours_deferred, throttled, sent, delivered, bounced, opened, clicked, paid, paused, retried, failed) with timestamps and event_ids. Given a recipient row, When expanded, Then it displays channel, address, provider_message_id(s), variant_id, wave_id, final_outcome, and error_details where applicable; entries map 1:1 to audit events. Given cross-navigation, When clicking a campaign_id or wave_id in the timeline, Then the app navigates to the corresponding dashboard pre-filtered to that scope. Given time display preferences, When toggled, Then timestamps switch between UTC and recipient local time without altering stored values.
Export CSV/JSON & Retention Policies
Given a user with export permission, When requesting an export for a campaign/wave or recipient drill-down, Then the system produces CSV and JSON exports with a documented schema and column headers, and records an export_requested audit event. Given datasets up to 1M rows, When export is requested, Then the file is generated within 5 minutes or streams progressively; exports over 1M rows stream and include byte-range support. Given PII and role policies, When generating exports, Then fields are redacted or excluded per role, and the export manifest lists included/excluded fields. Given retention policy N days and no legal hold, When events age beyond N, Then a purge job removes audit/reporting data and expired exports; a purge_completed audit event logs counts and ranges; legal holds prevent purge and are logged. Given an export completes, When downloaded, Then URLs expire within 24 hours and include a checksum to verify integrity; failures emit export_failed audit events with error codes.
Role-Based Access Controls for Audit & Reporting
Given defined roles (Admin, Manager, Auditor, BoardViewer), When accessing dashboards, audit logs, and exports, Then permissions are enforced: Admin/Manager full, Auditor read-only including exports, BoardViewer read-only dashboards without exports or PII fields. Given insufficient permissions, When a user attempts to view audit details or export, Then access is denied with 403 and a permission_denied audit event logs user_id, role, endpoint, and scope. Given API tokens with scopes, When calling audit/reporting endpoints, Then required scopes are validated and responses omit fields outside scope; row-level filters restrict data to the user's communities. Given role changes, When applied by an Admin, Then changes take effect within 60 seconds across UI and API and are logged with before/after permissions.

Forward Lock

Make every Pay‑by‑Text link one‑time, device‑bound, and time‑limited. If a link is forwarded or opened from a new device, it safely gates with a quick SMS code or unit check. Admins set expiry windows and open limits, while Duesly logs every attempt for audit clarity. Result: no logins needed, far less fraud and misapplied payments.

Requirements

One-time Link Tokenization & Lifecycle
"As a resident receiving a pay-by-text link, I want the link to work once and then be disabled so that I can pay securely without creating an account or risking reuse by others."
Description

Generate cryptographically signed, single-use pay-by-text tokens that bind to a payment intent and resident account context. Enforce one-time redemption, idempotency, and server-side invalidation upon successful payment or explicit revoke. Handle concurrent clicks and race conditions with optimistic locking and atomic state transitions. Return clear HTTP states for clients (e.g., active, used, expired, revoked) and surface human-readable messages in the UI. Store minimal metadata needed for enforcement and privacy, and support immediate revocation by admins. Integrate with existing payment processors and message delivery services without requiring login creation.

Acceptance Criteria
Secure Token Generation, Binding, and Minimal Metadata
Given an admin requests a pay-by-text link for a specific resident and invoice When the API creates the link Then the response contains a cryptographically signed tokenized URL and the database stores only token_hash, token_id, payment_intent_id, resident_id, created_at, expires_at, max_opens, state=active, and no plaintext token or PII. Given the token is altered or forged When verification is attempted Then signature verification fails and the API returns HTTP 401 with error=Invalid Token and no PII in the payload. Given token creation When inspected Then token entropy is ≥128 bits and expires_at is set according to the admin-configured window. Given a client checks link status When GET /token/:t/status is called for an active token Then the response returns state=active, amount_due, masked unit identifier, and no names/emails in ≤300ms p95.
Single-Use Redemption and Idempotency Enforcement
Given an active token bound to a payment intent When the resident completes a successful payment Then the system atomically transitions token state to used and prevents further redemption attempts. Given ≥5 concurrent redemption requests for the same token within 100ms When processed Then exactly one returns HTTP 200 success and others return HTTP 409 with state=used, and only one charge is created at the processor. Given a network timeout after submitting payment with an idempotency key When the user retries with the same key Then no additional processor charge is created and the same receipt is returned if the original succeeded. Given a payment authorization is declined by the processor When handled Then token state remains active and the user can retry until expires_at or revoke.
Device Binding and Forward-Gating Without Login
Given an active token with no bound device When first opened on a device Then the token binds to that device fingerprint and the user may proceed to payment without creating a login. Given the same token is opened on a different device prior to redemption When accessed Then an SMS code to the phone on file or a unit check challenge is required; on correct verification access is granted; on 3 consecutive failures access from that device is blocked for 15 minutes without changing token state. Given a token in state=used When opened from any device Then the response indicates state=used and no challenge is sent. Given a successful forward-gating verification When completed Then the verified device is recorded as allowed for this token and an audit log entry is created.
Expiry Window and Open Limits Enforcement
Given now > expires_at for a token When the link is accessed Then the API returns HTTP 410 with state=expired and the UI displays a message with a path to request a new link. Given admin sets max_opens=N for a token When total opens exceed N before redemption Then the token transitions to state=revoked (reason=open_limit) and subsequent requests return HTTP 403 revoked. Given a token is expired or revoked When a redemption attempt is made Then no call to the payment processor is made and no charge is created. Given a token with ≤24 hours remaining When the link is opened Then the UI displays the time-to-expiry warning without changing token state or incrementing open count beyond the current view.
Immediate Admin Revocation and Auditability
Given an active token When an admin revokes it via UI or API Then the token transitions to state=revoked within 3 seconds and subsequent requests return HTTP 403 with state=revoked. Given a revoked token is accessed When logging the attempt Then an audit record captures token_id (hashed), actor, timestamp, originating IP/device hash, and reason=admin_revoke. Given a token has been revoked When queried by status endpoint Then it cannot be reactivated and the response explicitly indicates revoked with a human-readable message.
Clear Client States and Human-Readable Messages
Given any token state When GET /token/:t/status is called Then the API returns one of {active, used, expired, revoked} only, mapped to HTTP {200, 409, 410, 403} respectively. Given a state is returned When rendered in the UI Then the user sees: Active="Secure link to pay your dues", Used="This link was already used", Expired="This link has expired", Revoked="This link is no longer valid"; messages are localized and contain no PII. Given an invalid or malformed token When requested Then the API returns HTTP 401 Invalid Token and the UI shows a generic invalid link message. Given a successful redemption When the receipt is displayed Then the token status endpoint returns state=used and the UI shows masked identifiers only.
Processor and Webhook Integration Without Login
Given a successful payment created at the processor using the tokenized context When the processor webhook is received Then the system finalizes the payment intent, marks the token as used if not already, and records the processor charge_id in the audit log. Given delayed or out-of-order webhooks from the processor When processed Then token state and payment intent reflect the correct final outcome without duplicate charges. Given SMS delivery is temporarily unavailable for forward-gating When gating fails to deliver a code Then the UI offers a unit check fallback and the user is not prompted to create or log into an account. Given capture fails after authorization When the processor returns a capture error Then the token remains or reverts to state=active, no duplicate authorizations occur, and the user can retry capture without regenerating the link.
Device Binding & Recognition
"As a resident, I want my payment link to open seamlessly on my device but challenge others if it is forwarded so that my payment details remain secure."
Description

On first open, bind the token to the accessing device using a privacy-preserving device identifier based on a secure first-party cookie and signed device-binding token. On subsequent opens, transparently allow access when the device matches; when it does not, route to a gate. Support degraded recognition when cookies are blocked using heuristic signals without storing fingerprints that can track users across contexts. Hash and salt identifiers at rest, avoid storing raw user agents, and rotate binding secrets regularly. Provide a safe unbind flow after successful payment or explicit revoke, and ensure behavior is consistent across iOS/Android/SMS browsers.

Acceptance Criteria
First Open Binds Device Via Cookie + Signed Token
Given a valid Pay‑by‑Text link is opened for the first time on an unbound device When the landing page initializes Then a first‑party cookie with attributes Secure, HttpOnly, and SameSite=Lax (or stricter) is set And a server‑issued device‑binding token tied to the link is generated and signed with the current binding secret And no PII or raw user agent is embedded in the token payload And an audit event "BOUND" is recorded with timestamp, link ID, and a salted‑hash device ID
Subsequent Open on Bound Device Allows Transparent Access
Given the device is already bound to the link When the link is opened again within the validity window Then access to the payment page is granted without presenting a verification gate And the same salted‑hash device ID is recognized server‑side And an audit event "MATCH" is recorded including timestamp and link ID
Mismatched Device Routes to Gate
Given the link is opened on a device without the binding cookie or with a mismatched binding token When the mismatch is detected Then the user is routed to the verification gate And the gate offers SMS code challenge or unit‑check alternative per configuration And access is denied until the user passes the gate And an audit event "MISMATCH_GATE" is recorded with attempt count (masked) and outcome
Degraded Recognition Without Cookies
Given the browser blocks first‑party cookies or the user has disabled cookies When the link is opened Then the system uses non‑persistent heuristic signals to assess device consistency without storing a cross‑context fingerprint And no raw user agent string is stored; only minimal, salted/hashed aggregates are kept at rest And the user is routed to the verification gate unless heuristics indicate a confident match within the same session And an audit event "DEGRADED_MODE" is recorded
Secure Storage and Secret Rotation
Given any device‑binding identifiers are stored Then they are hashed and salted at rest and never stored in raw form And raw user agent strings are not persisted in logs or databases And binding secrets are rotated on a regular schedule with a key ring that honors existing tokens during a grace period And rotation events are logged without exposing secret material And tokens issued after rotation validate only against active keys
Safe Unbind After Payment or Revoke
Given a payment is successfully completed via the link When the payment confirmation is issued Then the device binding for that link is unbound And subsequent opens of the link cannot initiate a new payment and route to receipt or a verification gate And an audit event "UNBOUND" is recorded with reason=payment_completed And Given an admin explicitly revokes the link When the revoke action is saved Then any existing bindings for the link are invalidated immediately And new opens route to the verification gate with a message indicating revocation
Consistent Behavior Across iOS/Android/SMS Browsers
Given the link is opened on iOS Safari, Android Chrome, or a default SMS in‑app browser When device binding and recognition flows execute Then first‑party cookie binding is used where supported; otherwise degraded recognition is used And gating behavior, unbind behavior, and audit logging produce identical outcomes across platforms for the same conditions And no platform exhibits a crash, infinite loop, or blocked payment path during binding, gate, or unbind
Configurable Expiry Windows & Open Limits
"As an HOA manager, I want to control how long a payment link stays active and how many times it can be opened so that I can balance convenience with fraud risk."
Description

Provide admin-configurable policies for token expiry duration and maximum opens. Support community-level defaults and per-link overrides, with validation, sensible presets, and API parity. Enforce limits server-side and show clear countdown timers and state messages to payers. Allow configuration for whether the open count increments per unique device or per request, with a recommended default. Include optional grace periods and automatic invalidation after payment completion. Persist policy versions with the token for accurate auditing.

Acceptance Criteria
Community-Level Defaults Configuration
Given I am an admin with Manage Payments permission and open Settings > Forward Lock Policy When I set an expiry duration within 5 minutes to 30 days using a preset (15m, 1h, 24h, 7d, 30d) or a valid custom value And I set a max open limit within 1 to 10 And I select a counting mode (per_device as the recommended default) and an optional grace period within 0 to 60 minutes Then the form validates ranges with inline errors for out-of-range or malformed values and disables Save until valid And saving persists the defaults server-side, returns 200, and GET /policies/forward-lock reflects the values And an audit log entry is created with a new policy_version and field-level changes And the new defaults apply only to tokens created after the save time
Per-Link Override at Token Creation
Given I am creating a Pay-by-Text link When I choose to override the community defaults and enter an expiry duration and open limit within allowed ranges And I optionally select a different counting mode and grace period Then the UI displays a summary chip of the override on the creation screen And the created token embeds the override policy snapshot and policy_version And POST /tokens supports the same override fields, rejecting invalid values with 422 and field-specific errors And tokens created with overrides are governed by the override rather than the defaults
Server-Side Enforcement and Payer Messaging
Given a payer opens a Pay-by-Text token URL When the token is valid (within expiry and below open limit) Then the page loads with a server-synced countdown timer showing time remaining and remaining opens And the open count is incremented according to the configured counting mode When the token is expired or has reached its open limit Then the server denies access and returns an error (410 expired or 429 open_limit_reached) And the UI shows a clear state message with guidance to request a new link And every attempt (allowed or denied) is logged with timestamp, device identifier (if available), and reason code
Open Count Mode Behavior
Given counting mode is per_device When the same device opens the token multiple times Then the open count increments once per device across the token lifetime And distinct devices each increment the count by one until the limit is reached And unknown or ambiguous device fingerprints are treated as new devices Given counting mode is per_request When the token URL is requested repeatedly from any device Then each successful page load increments the open count by one And upon reaching the configured limit, further opens are denied and logged with reason open_limit_reached
Grace Period Handling
Given a token has an expiry time and a configured grace period greater than 0 When the current time is after expiry but within expiry plus grace period Then the payer can still access and complete payment And the UI displays a grace period banner with minutes remaining and a countdown synced to the server When the current time exceeds expiry plus grace period Then access is denied with error expired and the attempt is logged with within_grace=false And analytics reflect the number of payments completed during grace
Automatic Invalidation After Payment
Given a token is used to complete a successful payment When the payment is confirmed server-side Then the token is immediately invalidated with status already_paid And subsequent opens return 410 already_paid and do not increment the open count And all further payment attempts with the token are rejected idempotently with the original receipt reference And an audit log entry records the invalidation event with policy_version and payment reference
Policy Version Persistence and Auditability
Given a token is created When the token is persisted Then it stores policy_version and a normalized snapshot of the applied policy (expiry, open limit, counting mode, grace setting) And subsequent changes to community defaults do not change the behavior of existing tokens And GET /tokens/{id}/audit returns the token policy snapshot and a chronological list of access attempts with decisions And policy versions referenced by tokens cannot be deleted; attempts return 409 conflict
SMS Code Gate & Unit Verification
"As a resident opening a forwarded link, I want to verify quickly via a code or unit check so that I can still pay without logging in while keeping my account safe."
Description

When a token is opened from an unrecognized device or exceeds policy thresholds, present a lightweight gate that verifies the payer via one-time SMS code to the phone on file or, when unavailable, a unit check (e.g., unit number and last name or recent invoice amount). Implement configurable retry and rate limits, short code expiry, and lockouts on repeated failures. Ensure accessibility, localization, and mobile UX best practices. Do not require account creation or password entry. Log challenge outcomes and allow success to temporarily whitelist the new device for the token lifecycle without altering the original device binding.

Acceptance Criteria
SMS Code Gate on Unrecognized Device
Given a valid Pay-by-Text token bound to Device A and a policy requiring verification on unrecognized devices And the token link is opened on Device B that is not recognized for this token When the payer opens the link on Device B Then the system displays the SMS code challenge without requiring account creation or password entry And an SMS containing a single-use 6-digit numeric code is sent to the phone number on file And the UI masks the destination phone number to the last 4 digits And the system enforces the configured send rate limit and cooldown for code delivery And a challenge_initiated event is written to the audit log with token ID, hashed device ID, trigger reason, and timestamp
OTP Entry, Expiry, Retry, and Lockout
Given an active SMS code challenge with code TTL, retry limit, and lockout policy configured When the payer enters the correct code within the TTL Then the verification succeeds and the payer is advanced to the payment screen And a challenge_succeeded event is written to the audit log When the payer enters an incorrect code Then an error message is shown without revealing partial correctness And the attempt count increments toward the configured retry limit When the number of incorrect attempts reaches the configured limit within the evaluation window Then the challenge is locked for the configured lockout duration And subsequent attempts during lockout are blocked with a lockout message And a challenge_lockout event is written to the audit log When the code is entered after the TTL expires Then the system rejects the code as expired and offers a resend option subject to send rate limits And a code_expired event is written to the audit log
Fallback Unit Verification When SMS Unavailable
Given the phone number on file is missing or undeliverable or the payer opts to use unit verification after failed SMS attempts When the payer selects unit verification Then the system presents fields for unit number and last name And the system requires a second factor of proof configured by the admin such as the most recent invoice amount And the input is validated against records for the target unit without revealing stored values And attempts are limited by the configured retry and lockout policies And a challenge_method_switched event is written to the audit log And on successful match the payer is advanced to the payment screen and a challenge_succeeded event is written
Temporary Device Whitelisting for Token Lifecycle
Given Device B passes verification for Token T When Token T is subsequently opened on Device B within Token T’s validity window Then the verification gate is bypassed for Device B for Token T And Device A remains recognized for Token T And the whitelist for Device B is purged when Token T expires And opening any other token on Device B continues to require verification according to policy And no persistent account or password is created or required
Accessibility, Localization, and Mobile UX Compliance
Given the verification gate is displayed on a mobile device When evaluated against WCAG 2.2 AA criteria Then all interactive elements meet contrast, focus, labeling, and keyboard navigation requirements And status messages are announced via ARIA live regions And the UI is fully operable with screen readers When the locale is set to a supported non-English language Then all gate UI text and SMS message content are localized to that locale And dates, times, numbers, and currency amounts follow locale formats And if a translation is missing the system falls back to English without breaking layout When the payer enters the SMS code Then a numeric keypad is invoked, auto-advance between digits is supported, OS OTP autofill is accepted, and touch targets are at least 44x44 points
Audit Logging and PII Minimization
Given any verification challenge lifecycle event occurs When the event is recorded Then the audit log entry includes event_type, token_id, hashed device identifier, unit ID, trigger_reason, method_used, attempt_count, outcome, timestamp, and locale And sensitive fields such as phone numbers and names are not stored in full; phone numbers are masked to last 4 digits And log entries are immutable and retrievable via admin UI or API with filters by token, unit, and date range And access to logs is restricted to authorized roles
Gate Trigger on Policy Thresholds
Given an admin has configured open limits and expiry windows for Pay-by-Text tokens And policy requires verification when opens exceed the configured limit or occur outside the allowed window When a token is opened under conditions that exceed the open limit or outside the allowed window Then the verification gate is presented according to the configured method availability And the trigger_reason is recorded in the audit log as policy_threshold_exceeded And openings within limits on a recognized device proceed without gating
Attempt Logging & Audit Trail
"As an HOA manager, I want a clear audit trail of payment link activity and challenges so that I can resolve disputes and demonstrate compliance."
Description

Record every token-related event, including sends, opens, device matches or mismatches, gates presented, verification results, payments, expirations, revocations, and policy values at decision time. Store timestamps, coarse device characteristics, and IP city-level geodata while redacting or hashing sensitive attributes. Provide an admin UI and export API to search, filter, and export logs by community, unit, token, or time range. Include correlation IDs for cross-system tracing and optional webhooks for SIEM integration. Apply retention policies aligned with compliance and storage costs.

Acceptance Criteria
Token Event Lifecycle Logging
- Given Forward Lock is enabled for a community, when any token is created, sent, opened, gated, verified, paid, expired, or revoked, then a log event is written for each of the following types: token_created, token_sent, link_opened, device_match, device_mismatch, gate_presented, verification_passed, verification_failed, payment_initiated, payment_succeeded, payment_failed, token_expired, token_revoked. - Each event includes: event_time (UTC ISO-8601 with ms), community_id, unit_id, token_id, correlation_id, event_type, actor (system|resident|admin), policy_snapshot (expiry_window_minutes, open_limit, remaining_opens at decision time), result_status (success|failure), reason_code (nullable), device_coarse (ua_family, os_family), device_id_hash, ip_geo (city, region, country), and idempotency_key (nullable). - Events are persisted within 2 seconds of the action and are retrievable via UI/API within 10 seconds; no event loss across retries (at-least-once semantics) and events are query-sortable by event_time ascending/descending. - 100% of the described flows (happy-path and error-path) produce the expected events in a staging test matrix across iOS/Android/SMS and desktop/mobile browsers.
Sensitive Data Redaction & Hashing
- No plaintext tokens, full phone numbers, full IP addresses, device fingerprints, or email addresses are stored in logs. - Token values are never logged; only token_hash_prefix (first 8 chars of SHA-256) is present. - Phone numbers are stored as phone_last4 and phone_hash (SHA-256 with secret salt); IPs are reduced to ip_geo (city, region, country) only; device identifiers are stored as device_id_hash (SHA-256 with secret salt); user agent strings are reduced to ua_family and os_family. - Hashing uses an irreversible algorithm (SHA-256) with a tenant-scoped secret salt; salts are rotated without breaking historical correlation (previous salt retained for verification during retention period). - Automated tests confirm no disallowed fields appear in serialized events, and a redaction unit test suite passes with 100% coverage of sensitive attribute cases.
Admin Audit UI: Search, Filter, and View
- Admins with the Audit permission can access a Logs screen scoped to their community; non-admin users cannot access it (403). - UI supports filters: community (if multi-portfolio admin), unit_id, token_id, correlation_id, event_type (multi-select), result_status, date range (UTC, quick-picks and custom), device_mismatch flag. - Results list displays: event_time (localized to admin’s timezone), event_type, unit, token_id (short), correlation_id, result_status, ip_geo, device_coarse; clicking a row reveals full event details including policy_snapshot and reason_code. - Pagination or infinite scroll returns stable results; 95th percentile time-to-first-page < 2s for a dataset of 50k events; sorting by event_time works both directions. - From the UI, admins can copy correlation_id and export the filtered result set to CSV within the UI (handoff to Export API).
Export API for Logs
- An authenticated API allows export of logs by community_id, unit_id, token_id, correlation_id, event_types[], result_status, start_at, end_at; supports pagination via cursor and page_size up to 10,000. - Exports can be synchronous for ≤10,000 rows (CSV) and asynchronous for larger datasets up to 1,000,000 rows, returning a job_id and later a signed download URL valid for 24 hours. - Exported schema includes all public fields from the log event with documented headers; timestamps are UTC ISO-8601; line endings are LF; CSV is UTF-8 with header row. - API enforces rate limits (429 on exceed) and validates parameter ranges; partial exports are not produced—completeness is guaranteed or the job fails with an error code. - Checksums (SHA-256) are provided for exported files; exports respect retention and permissions (no cross-community leakage).
Correlation IDs & Cross-System Traceability
- A correlation_id (UUIDv4 or ULID) is generated at token creation and attached to every subsequent event in the token’s lifecycle, including payment gateway callbacks and SIEM webhooks. - Searching by correlation_id returns a contiguous, time-ordered sequence of all related events across systems (send→open→gate→verify→payment→final state) with no gaps under normal operation. - Correlation_id uniqueness is enforced system-wide; probability of collision is less than 1e-12, validated by property-based tests over 10 million generated IDs. - Payment events include external_reference fields (e.g., gateway transaction_id) enabling cross-reference from Duesly to processor logs; these references are present in 100% of payment_succeeded/failed events.
SIEM/Webhook Delivery for Audit Events
- Communities can configure zero or more webhook endpoints for audit events with per-endpoint enable/disable and event_type filters. - Webhook payloads are signed with HMAC-SHA256 using a per-endpoint shared secret; headers include signature and timestamp; receivers can verify within a 5-minute clock skew. - Delivery is at-least-once with exponential backoff (e.g., 1m, 5m, 15m, 60m) up to 24 hours; after max retries, events are moved to a dead-letter queue visible in the UI. - Payloads include correlation_id and all non-sensitive event fields; batch mode delivers up to 100 events or 1 MB per request, whichever comes first. - An integration test confirms successful receipt, signature verification, and deduplication behavior with idempotency keys; failed deliveries are logged with reason_code.
Retention Policies & Purging
- Retention is configurable per community within an allowed range (180–730 days), defaulting to 365 days; current effective policy is shown in the UI and logged in policy_snapshot on decision events. - A daily purge job deletes events older than the retention window and produces a purge_audit event summarizing counts removed per community and time window. - Legal hold can be applied at community or correlation_id scope to suspend purging; holds are auditable and require admin justification text. - Purged data is irrecoverable from application APIs and UI; exports and queries after purge return no results older than the retention cutoff. - Performance of purge maintains DB health: job completes within maintenance window and keeps primary query P95 < 2s on a dataset of 100M events.
Seamless Payment Flow Integration & Messaging
"As a resident, I want clear guidance when a link is expired or gated so that I can complete my payment quickly without confusion."
Description

Integrate Forward Lock checks ahead of the payment form to avoid loading sensitive components until the token is validated. Provide clear, friendly messaging for states such as expired, already used, or gated, with options to request a fresh link from admins when appropriate. Maintain idempotent payment submission and ensure performance budgets for first-load on typical mobile networks. Support branding and content customization per community without changing core enforcement. Ensure the mechanism is compatible with the existing pay-by-text delivery and tracking pipeline.

Acceptance Criteria
Gate Payment Form Until Forward Lock Validation Passes
Given a user opens a Pay‑by‑Text link with a Forward Lock token When the page loads Then the app calls the Forward Lock validation API before initializing any payment form iframe/SDK And Then no network requests are made to payment processor endpoints (/tokenize, /payment-intents, /charges) until validation returns a VERIFIED state When validation returns INVALID, EXPIRED, USED, or DEVICE_GATED Then the payment form bundle is not loaded and a state screen is shown instead And Then an audit event is recorded with fields: token_id, device_id/fingerprint, state, timestamp, ip, user_agent
Clear Messaging for Expired or Already Used Links with Fresh Link Option
Given the token state is EXPIRED When the user opens the link Then show an "Link expired" message with community name/logo and human‑readable expiration time And Then display a "Request a new link" call‑to‑action if allow_link_renewal = true When the user taps the request button Then a renewal request is queued to admins and an on‑screen confirmation is shown; no payment form is rendered Given the token state is USED When the user opens the link Then show a "Link already used" message with last‑used date/time and hide the payment form And Then present the configured contact/CTA (support email/phone or renewal request) and log the selection And Then the state screen meets WCAG AA (contrast, focus order, screen‑reader labels) and uses community‑configurable text slots
Device‑Bound Check with SMS Code or Unit Verification
Given a valid token is opened on a device different from the device originally bound or the open_limit is exceeded When the page loads Then show a verification gate instead of the payment form When SMS verification is selected Then send a 6‑digit OTP via SMS to the payer phone on file; TTL = 10 minutes; max 5 attempts per token/device with exponential backoff and a 10‑minute lockout on max attempts When the correct OTP is entered within TTL Then mark the current device as bound, increment open_count, and load the payment form When SMS is unavailable or disabled Then present unit + last name verification; only an exact match allows proceed; max 3 attempts before lockout And Then all verification attempts (success/failure) are logged with token_id, device_id, method, outcome, timestamp, ip
Community‑Level Configuration: Expiry, Open Limits, Branding
Given an admin with appropriate permissions When they edit Forward Lock settings for a community Then they can set: expiry_window (15 minutes–30 days), open_limit (1–5), allowed gating methods (SMS, Unit Check fallback), and customize message copy/CTA, logo, and brand colors When settings are saved Then changes apply to newly issued tokens within 5 minutes without a deployment; existing tokens retain their original expiry and limits When invalid values are entered Then the UI blocks save and shows inline validation with accepted ranges And Then all changes are recorded in an audit log with admin_id, before/after values, timestamp And Then per‑community settings are isolated; changes do not affect other communities or core enforcement logic
Idempotent Payment Submission and Duplicate Protection
Given a verified session When the user submits a payment Then the request includes an idempotency key derived from (token_id + invoice_id + payer_id/device_id) and the same key is used on retries When the submit action is retried (network retry, double‑tap, back/forward) Then no additional charge is created and the prior successful result is returned within 2 seconds When the processor returns 5xx or 429 Then the client retries with exponential backoff up to 3 times using the same idempotency key When a prior payment exists for the same invoice and idempotency key within 24 hours Then the UI surfaces the existing confirmation and disables the submit button And Then all submissions and outcomes are logged with correlation_id = idempotency_key
First‑Load Performance Budgets on Mobile Networks
Given a typical 4G network profile (RTT 300–400 ms, 1.6 Mbps) When the gating screen loads Then TTI <= 2.5 s p75 (<= 3.5 s p95), LCP <= 2.5 s p75, and total JS for the gate route < 150 KB gzipped When Forward Lock validation succeeds and the payment form route initializes Then additional JS required before form mount is < 50 KB gzipped and total blocking requests <= 8 And Then primary text/skeleton is visible within 1 s p75 And Then RUM metrics are captured and reported with community and release tags, with alerts when budgets are exceeded for 15 minutes
Compatibility with Existing Pay‑by‑Text Delivery and Tracking
Given an SMS sent via the existing pay‑by‑text pipeline When the recipient taps the link Then the click is attributed to the original message/campaign ID and appears in existing analytics with the same session/click IDs And Then UTM/tracking parameters are preserved through the Forward Lock gate and are present on the payment confirmation pageview And Then no changes are required to SMS provider templates; legacy (ungated) links still function when Forward Lock is disabled per community And Then delivery receipts, CTR, and conversion metrics remain available; new gate events (validated, expired, used, gated, verified) are additive and do not break existing reports And Then desktop openings of SMS links follow the same gating and tracking logic
Security Hardening & Abuse Mitigations
"As a product owner, I want Forward Lock to include robust security safeguards so that we reduce fraud and operational risk without adding user friction."
Description

Protect the feature with rate limiting per IP/device, token scope validation, HMAC/JWT signing using KMS-backed rotating keys, strict TLS, and replay protection. Prevent token enumeration via non-predictable IDs and uniform error responses. Implement bot detection on gate endpoints, audit key usage, and create runbooks and monitors for anomaly detection (e.g., spike in failed gates). Store secrets in a secure vault, enforce least-privilege access, and perform threat modeling and penetration tests before launch.

Acceptance Criteria
Rate Limiting on Gate Endpoint Under Burst Traffic
Given the /forward-lock/gate endpoint enforces per-IP 60 requests/min with burst 20, per-device 20 requests/min with burst 10, and per-token 5 opens/24h When a single IP sends 100 requests within 60 seconds Then at least 40 requests are rejected with HTTP 429 and include a Retry-After header and the rejection is logged with ip_hash and device_hash Given a device sends 30 gate attempts within 60 seconds When the per-device threshold is exceeded Then subsequent requests within the minute receive HTTP 429 and successful requests resume automatically after the window resets Given a valid user completes 1 gate attempt in 10 seconds When under the configured limits Then no throttling occurs and the request latency p95 remains under 250 ms
Token Signing, Scope Validation, and KMS Key Rotation
Given Forward Lock tokens are signed via KMS-managed keys with automatic rotation every 90 days and a 14-day verification overlap When a token is signed with the current or immediately previous active key Then the token verifies successfully Given a token signed with an unknown, disabled, or expired key When presented to any verification endpoint Then the token is rejected and the event is recorded in key-usage audit logs Given a token contains claims for community_id, unit_id, invoice_id, device_id, and exp When the resource identifiers in the request do not match the token scope Then the request is rejected Given services retrieve keys and secrets only via the secure vault and IAM role-based access When static analysis scans the repository and runtime environment Then no plaintext secrets or long-lived credentials are present and service roles have least-privilege permissions limited to sign/verify on the specific KMS key
Transport Layer Security Hardening for Pay-by-Text and Gate Endpoints
Given any HTTP request to pay-by-text or gate endpoints When received over plaintext HTTP Then it is redirected with HTTP 301 to HTTPS and no sensitive data is processed pre-redirect Given TLS is negotiated When clients connect Then only TLS 1.2+ with strong ciphers is accepted and SSL Labs grade is A or higher Given a successful HTTPS response When headers are inspected Then Strict-Transport-Security is present with max-age >= 15552000; includeSubDomains Given the pay-by-text pages load external assets When the page is rendered Then there is no mixed-content reported by the browser security console
Replay Protection and Enumeration Resistance
Given a one-time Forward Lock token with jti and nonce bound to device_id When it is used successfully once Then any subsequent use returns the same generic gate failure response and is logged as replay_attempt Given the token is time-limited to a configurable expiry (default 24h) with ±5 minutes clock skew tolerance When the token is used after expiry Then it returns the same generic gate failure response and no token validity details are leaked Given invalid, expired, mismatched-scope, or bad-signature tokens When presented to validation endpoints Then the HTTP status and response body are identical across all failure types and server-side processing time p95 does not vary by more than 50 ms between failure classes Given token identifiers and links are generated When 10,000 tokens are created in a batch Then each token contains at least 128 bits of entropy and no sequential or incremental patterns are detected
Bot Detection and Challenge on Gate Endpoints
Given bot detection is enabled on /forward-lock/gate When requests originate from headless automation signals or exceed velocity thresholds (e.g., >10 failures/60s per IP) Then a challenge (e.g., SMS code or equivalent) is required and non-humans cannot proceed without solving it Given a legitimate user on a modern browser with a valid token under normal velocity When accessing the gate Then no challenge is shown and the flow completes without added friction Given challenges are presented When solved correctly within 2 minutes Then access proceeds; when failed or timed out, access is denied with a generic response and the attempt is logged with risk_score
Audit Logging, Monitoring, and Incident Runbooks
Given any gate attempt, token validation, or key operation When the event occurs Then an immutable log entry is written with timestamp, ip_hash, device_hash, token_jti_hash, outcome, and reason_code without storing PII Given operational monitors are configured When failed gate attempts exceed 3x the 7-day baseline for 5 consecutive minutes or HTTP 429 rate > 20% for 5 minutes Then a P1 alert is sent to on-call within 2 minutes with a link to dashboards and runbooks Given dashboards for security metrics When observed by operators Then they display time-series for success/failure, replays, bot challenges, rate-limit hits, key verifications, and anomaly flags with 1-minute resolution Given runbooks exist for spikes in failed gates and suspected enumeration When an alert fires Then responders can follow step-by-step actions to mitigate within the defined SLO (investigation start < 15 minutes)
Threat Modeling and Penetration Test Exit Criteria
Given a STRIDE-based threat model for Forward Lock When reviewed in a security design review Then all Critical/High risks have implemented mitigations or documented, approved exceptions prior to launch Given an external penetration test covering token replay, enumeration, TLS downgrade, rate limiting, and bot bypass When the final report is delivered Then there are zero open Critical/High findings and all Medium findings have remediation plans and due dates Given remediation commits are deployed When the tester performs a retest Then all previously identified issues in scope are verified as fixed or formally accepted Given the launch gate checklist When evaluated Then release is blocked until the above conditions are met and approvals are recorded

Wallet SmartOpen

Detects the recipient’s device and launches Apple Pay or Google Pay automatically with the amount, memo, and due date prefilled. If a wallet isn’t available, it falls back to a secure card/ACH screen or a saved method from prior payments. Fewer taps, faster completes, and higher mobile conversion.

Requirements

Smart Wallet Detection & Routing
"As a homeowner, I want the payment link to automatically open the right wallet on my device so that I can pay my dues with the fewest possible taps."
Description

Implement client- and server-side detection to determine device OS, browser capabilities, and wallet availability, then route payers to Apple Pay, Google Pay, or a secure web checkout accordingly. Use native wallet readiness checks (e.g., canMakePayments / isReadyToPay) and fall back gracefully when wallets are unavailable or blocked (desktop, unsupported browsers, in-app webviews). Maintain a single payment intent identifier across all routes to preserve idempotency and reconciliation. Handle deep links from feed posts, emails, SMS, and push notifications, ensuring the correct context (amount, memo, due date, payer identity) is carried through. Provide robust error handling and recovery paths (retry wallet, switch method, continue on web) with clear user messaging. Respect regional availability and feature flags at the community level.

Acceptance Criteria
iOS Safari Apple Pay Ready Launch
Given a payer opens a Duesly payment link on iOS Safari and ApplePaySession.canMakePayments() returns true and the community has Wallet SmartOpen enabled and the region supports Apple Pay When the payer taps Pay Now Then the Apple Pay sheet presents within 1000 ms with amount, memo, and due date prefilled from the payment context and the merchant display name matches the community name And the payer identity (userId or tokenized memberId) is attached to the payment intent before authorization And upon authorization, the payment is captured using the same payment_intent_id and a receipt screen is shown within 2000 ms And analytics events are recorded: wallet_route=apple_pay, readiness_check=true, time_to_sheet_ms<=1000, payment_intent_id, community_id
Android Chrome Google Pay Ready Launch
Given a payer opens a Duesly payment link on Android Chrome and Google Pay isReadyToPay returns true and the community has Wallet SmartOpen enabled and the region supports Google Pay When the payer taps Pay Now Then the Google Pay sheet presents within 1000 ms with amount, memo, and due date prefilled and merchant information correct And upon authorization and tokenization, the payment is captured using the same payment_intent_id and a receipt screen is shown within 2000 ms And analytics events are recorded: wallet_route=google_pay, readiness_check=true, time_to_sheet_ms<=1000, payment_intent_id, community_id
Unsupported Environment Fallback to Secure Checkout or Saved Method
Given the payer opens a payment link in an environment without usable wallets (e.g., desktop browser without wallet support, in-app webview, or readiness checks return false) When the payer taps Pay Now Then the experience routes to secure web checkout over TLS, with card and ACH options visible And if a saved payment method exists for the payer, it is auto-selected and requires only necessary confirmation (e.g., CVV/affirmation) to submit; otherwise, full entry fields are shown And wallet buttons are not rendered And amount, memo, due date, and payer identity are prefilled and immutable except allowed fields And the checkout becomes interactive within 2000 ms on a 4G connection And analytics events are recorded: wallet_route=web_checkout, saved_method_used (true/false), environment=unsupported
Single Payment Intent Persistence Across Routes and Retries
Given a payer initiates a payment via any route (Apple Pay, Google Pay, or web checkout) When the payer cancels the wallet sheet or encounters an error and switches methods or retries within 15 minutes Then the same payment_intent_id persists across all attempts and is used for final capture And duplicate charges are prevented via idempotency keys at the gateway and server And only a single intent record exists in reconciliation, linked to the originating post/invoice And audit/telemetry show a single intent with multiple attempt events and final status
Deep Link Context Preservation Across Entry Points
Given a payer opens a payment deep link from a feed post, email, SMS, or push notification When the link resolves and the payment experience initializes on the device Then the context (amount, memo, due date, post/invoice id, payer identity or claim token) is present and validated before showing wallet or checkout And if identity is missing/expired, the user is prompted to authenticate or re-claim via a one-tap link before payment can proceed And the context survives app switches and route changes without loss until payment completion or explicit cancel And tampering or invalid context is detected and results in a safe error state with no payment attempt created
Error Handling and Recovery With Clear Messaging
Given a readiness check fails, a wallet sheet fails to present, network connectivity is lost, or the gateway declines a transaction When the error occurs Then a clear, localized error message is shown with a recommended next action And the user is offered options: Retry Wallet, Switch Method (card/ACH), or Continue on Web, with amount, memo, and due date preserved And selecting any option proceeds without requiring the user to re-enter context and logs the action taken And telemetry captures error_code, action_taken, attempt_count, time_to_recover_ms, and final outcome
Regional Availability and Feature Flag Enforcement
Given the payer’s region or BIN country does not support Apple Pay and/or Google Pay, or the community has disabled Wallet SmartOpen via feature flags When the payment experience initializes Then disallowed wallet options are not rendered and no disallowed readiness checks are invoked And an allowed route is selected automatically (e.g., web checkout or remaining wallet) And legal copy and branding shown match the allowed methods for the region And an audit entry records region, flags evaluated, and the resulting route
Prefilled Payment Launch
"As a payer, I want the wallet sheet to show my dues amount and details automatically so that I can confirm and pay without manual entry."
Description

Generate a prefilled payment payload from the Duesly bill (amount, memo, due date, invoice ID, community name) and render the wallet sheet with those values populated. For Apple Pay and Google Pay, construct compliant requests including merchant identifiers, supported networks, currency, and total, with the memo mapped to the merchant display where supported. Enforce the billed amount by default while allowing admin-configurable options for tips or adjustments if enabled. Apply due date logic (e.g., late fee messaging) to the sheet subtitle or pre-review screen. Bind the payload to a server-side payment intent with expiration, preventing tampering and enabling idempotent capture. Validate currency and tax settings, and ensure payer identity is linked to the Duesly household record for receipt routing and audit.

Acceptance Criteria
Apple Pay Prefilled Wallet Launch
Given a billed invoice with amount, memo, due date, invoice ID, community name, and a supported currency And a device with Apple Pay available and at least one eligible card When the payer taps the payment action from the Duesly bill Then the Apple Pay sheet opens with merchant identifier, supported networks, currency, and total populated from the bill And the community name appears as the merchant display and the memo is shown where supported And the billed amount is locked unless tips/adjustments are enabled by admin And if tips/adjustments are enabled, the tip/adjust input appears with configured options and caps and the total reflects the selection And the due date and any late-fee messaging are displayed in the subtitle/pre-review text And the payment request includes a server-issued intent reference that is not modifiable client-side And canceling closes the sheet without authorizing or capturing funds And authorizing returns a token that is applied to the bound server intent for capture
Google Pay Prefilled Wallet Launch
Given a billed invoice with amount, memo, due date, invoice ID, community name, and a supported currency And an Android device or Chrome with Google Pay available and at least one eligible payment method When the payer taps the payment action from the Duesly bill Then the Google Pay sheet opens with merchant info, allowed networks, currency, and total populated from the bill And the community name appears as merchant info and the memo is mapped to a display/subtitle field where supported And the billed amount is locked unless tips/adjustments are enabled by admin And if tips/adjustments are enabled, the tip/adjust input appears with configured options and caps and the total reflects the selection And the due date and any late-fee messaging are displayed in the subtitle/pre-review text And the payment data request contains a server-issued intent reference that is not modifiable client-side And canceling dismisses the sheet without authorizing or capturing funds And authorizing returns payment data that is applied to the bound server intent for capture
Fallback to Card/ACH With Prefill and Saved Methods
Given a device/browser without a supported wallet or the payer declines the wallet sheet When the payer initiates payment from the Duesly bill Then the system presents a secure card/ACH screen or a previously saved payment method And the amount, memo, due date, invoice ID, community name, and currency are prefilled and displayed And the billed amount is locked unless tips/adjustments are enabled by admin And if tips/adjustments are enabled, the tip/adjust input appears with configured options and caps and the total reflects the selection And due date and late-fee messaging are displayed prior to submission And the form is bound to a server-issued payment intent with expiration And submitting with a saved method or new method authorizes against the bound intent
Server Intent Binding, Expiration, Idempotency, and Tamper Prevention
Given a server-side payment intent created for the bill with an expiration timestamp and idempotency key When the client requests a wallet launch or fallback form Then the client receives a signed, non-editable reference to the intent and amount And if the client attempts to alter amount, currency, invoice ID, community, or due date payload values, the server rejects the request and logs a tamper event without authorization And if the intent is expired at launch or authorization time, the request is denied with an expiration message and no authorization occurs And if the payer retries the same payment with the same idempotency key, only a single authorization/capture is performed and subsequent retries return the prior result And successful authorization marks the intent ready for capture and associates it to the bill record
Currency and Tax Settings Validation
Given community currency and tax settings configured in Duesly for the bill When constructing the wallet or fallback payment request Then the request uses the bill’s currency code and enforces the correct minor unit precision (e.g., 0-decimal currencies) And if the bill currency differs from the merchant configuration, the launch is blocked with a clear error and an audit log entry And required tax fields are validated and itemized or included per configuration, and totals reconcile exactly to the billed amount (+/− tips/adjustments if enabled) And the displayed total, currency symbol/code, and any tax indicators match the validated configuration
Payer Identity Linking and Receipt Routing
Given a payer initiates payment from a bill link associated with a Duesly household record When the payment is authorized (wallet or fallback) Then the payer’s identity (e.g., wallet-provided email/phone or entered details) is linked to the correct household record And the payment record includes invoice ID, household ID, and device identifier for audit And receipts are routed to the household’s preferred contact channels and appear in the household’s activity feed And if identity cannot be resolved automatically, the payer is prompted to confirm or select a household before completion
Admin-Configurable Tips and Adjustments Controls
Given an admin has configured tips and/or adjustments for the community When a payer opens a bill to pay (wallet or fallback) Then tips/adjustments controls are shown or hidden according to configuration And selectable options, min/max limits, and default values match configuration and are enforced client- and server-side And the total reflects the selection in real time and never falls below the billed amount unless a negative adjustment is explicitly allowed by config And disabling tips/adjustments in admin immediately hides controls for subsequent launches and rejects in-flight attempts to add them
Secure Fallback Card/ACH Checkout
"As a resident, I want a secure card or bank option when my device can’t use a wallet so that I can still complete payment without friction."
Description

Provide a mobile-first fallback checkout that supports card and ACH when a native wallet is unavailable or declined. Use tokenized, PCI SAQ-A compliant elements from the payment processor for card entry and secure bank linking for ACH, enforcing 3DS2/SCA and NACHA requirements as applicable. Reuse the same payment intent as the wallet flow to keep state consistent. Prefill amount, memo, and due date; display applicable fees and late status; and present clear CTAs to switch back to a wallet if it becomes available. Implement robust validation, error states, and retry logic, with graceful degradation in embedded webviews. Support guest checkout and authenticated users, with optional email/phone verification to tie the payment to the correct household.

Acceptance Criteria
Fallback Checkout When Wallet Unavailable or Declined
Given a device has no available native wallet or a wallet payment is declined When the user initiates payment via Wallet SmartOpen Then the fallback checkout displays Card and ACH options in the same session And the amount, memo, and due date are prefilled from the existing payment intent And applicable fees and late status are visible before method selection And a "Try Apple Pay/Google Pay" CTA is shown if a wallet becomes available during the session And selecting the CTA returns the user to the wallet flow using the same intent
Card Entry via Tokenized Elements and 3DS2/SCA Enforcement
Given the user selects Card on the fallback checkout Then all card fields are payment-processor-hosted/tokenized (SAQ-A compliant) And no PAN/CVV is stored, transmitted, or logged by Duesly systems When the issuer requires SCA Then a 3DS2 flow is initiated and can be completed within the fallback UI or a compliant redirect And upon successful authentication the original payment intent is confirmed per configuration And if 3DS fails or is canceled the user sees a clear error and can retry or switch methods without creating a new intent
ACH Checkout with Secure Bank Linking and NACHA Compliance
Given the user selects ACH on the fallback checkout When starting bank linking Then a secure, processor-approved bank-link flow opens inline or as an allowed modal And raw account/routing fields are never collected by native inputs And before confirmation the user is shown NACHA-required authorization text (amount, timing, revocation) and must explicitly agree And if instant verification is unavailable the flow supports micro-deposits with clear status and re-entry And upon submission the payment intent reflects ACH processing timelines and potential returns windows
Payment Intent Reuse and Idempotency Across Flows
Given a payment intent is created by the wallet flow When the user switches to the fallback checkout or back to a wallet Then the same payment intent ID is reused with amount, memo, due date, fees, and metadata preserved And idempotency keys are applied to confirm/capture to prevent duplicate charges And only one terminal state (succeeded, failed, or canceled) can be reached And all state changes are audit-logged with actor, timestamp, and source (wallet/fallback)
Prefill, Fees, Late Status, and Switch-to-Wallet CTA
Given the fallback checkout is displayed Then amount, memo, and due date are prefilled and read-only unless the intent permits edits And itemized fees (e.g., card surcharge, ACH fee) and the total are visible before confirmation And if past due a "Late" badge and any late fee are shown prominently And a persistent "Switch to Wallet" CTA is visible when a wallet is detected and returns the user to wallet confirmation using the same intent
Validation, Error States, Retries, and Webview Degradation
Given any form input is invalid Then inline accessible error messages identify the field and rule and the Pay button remains disabled until valid When processor/network errors occur (e.g., 3DS timeout, ACH link failure) Then the user sees a human-readable error and can retry up to 3 times with exponential backoff And a cancel action is offered that safely returns to the method selector without losing intent state And in embedded webviews, external auth flows render in-tab or fall back to "Open in browser" with state preserved
Guest and Authenticated Checkout with Contact Verification
Given an authenticated user Then household and contact data are prefilled and the payment auto-associates to the household And previously saved payment methods are offered where permitted Given a guest user When entering email or phone Then an optional OTP verification flow can be invoked to associate the payment to the correct household And guests can complete payment without account creation, with PII masked in UI and logs
Saved Method One‑Tap Confirm
"As a returning payer, I want to confirm with my saved payment method in one tap so that I can pay faster without re-entering details."
Description

When a payer has a previously saved, tokenized card or ACH method, present a one‑tap confirmation option with clear masking and method details. Require lightweight re‑authentication (e.g., OTP or device biometric if available) before charge, and allow switching to wallet or entering a new method. Respect user consent for storage and community policies; provide a simple manage/remove flow. Prefer wallets when available for the fastest path but surface saved methods as a fallback that preserves conversion. Ensure compliance with SCA/3DS for stored credentials and use network tokens where supported. Maintain security by scoping saved methods to the payer’s Duesly account/household and preventing cross-account exposure.

Acceptance Criteria
One‑Tap Confirm Displays for Saved Tokenized Method
Given an authenticated payer has at least one saved, tokenized card or ACH method scoped to their Duesly account/household When they open a payable item via Wallet SmartOpen or the Duesly checkout screen Then a One‑Tap Confirm UI is displayed by default with the most recently used or default saved method preselected And the method details are masked: cards show brand, last4, and expiry MM/YY; ACH shows bank name (or type) and last4 And the payable amount, memo, and due date are visible and match the invoice payload And a clearly labeled Pay CTA is present and enabled only for unpaid invoices and valid amounts per community rules And if no saved method exists, the One‑Tap option is not shown
Lightweight Re‑Authentication Gating Charge
Given One‑Tap Confirm is visible with a saved method selected When the payer taps Pay Then the system prompts for device biometric if available and enrolled; otherwise a 6‑digit OTP is sent to the last verified email or phone And the charge is attempted only after successful biometric or correct OTP within 2 minutes And failed attempts are limited to 3 before a 5‑minute lockout on One‑Tap for that invoice And all attempts are logged with a correlation ID without storing biometric or OTP secrets And idempotency prevents duplicate charges on retries for 15 minutes
Wallet Preferred With Seamless Switch From One‑Tap
Given the device supports and has Apple Pay or Google Pay provisioned When the payer opens the payment screen for an eligible invoice Then the wallet option is presented as the primary CTA with amount, memo, and due date prefilled And the One‑Tap saved‑method option is also visible as a secondary path And selecting wallet launches the native sheet with the payload prefilled; cancelling returns to the payment screen with state preserved And selecting Change Method opens a selector including wallet, saved methods, and Add New Method
Consent and Community Policy Enforcement for Stored Methods
Given community storage policies and the payer’s consent record are available When determining eligibility to display One‑Tap Then One‑Tap is shown only if the method type is allowed by policy and the payer has active consent with timestamp, version, and source recorded And if consent is missing or revoked, an inline prompt requests consent before enabling One‑Tap And if policy disallows storing ACH or card, those methods cannot be stored and are excluded from One‑Tap with an explanatory notice
Manage and Remove Saved Methods
Given the payer navigates to Manage Payment Methods from the payment screen or profile When the list of saved methods is displayed Then each method is masked and labeled; exactly one default can be set And removing a method disables or deletes the token at the processor and removes it from One‑Tap within 2 seconds And after removal the method is unavailable for all invoices and cannot be charged And the action shows a confirmation message and creates an auditable log entry
SCA/3DS and Network Token Handling for Stored Credentials
Given a One‑Tap payment is initiated with a saved card When the issuer requires SCA/3DS Then the transaction is flagged correctly for stored credentials (CIT or MIT with initial reference) and a 3DS2 challenge is presented inline And if the issuer allows frictionless flow the transaction completes without challenge And network tokenization is used where supported; lifecycle updates (e.g., PAN changes) are handled without payer action And 3DS results (eci, dsTransID, cavv) are stored on the payment record for audit
Account/Household Scoping and Cross‑Account Isolation
Given a saved method is stored for a specific payer’s Duesly account/household When a user outside that account/household views a payable or attempts to reference the method token via API Then the method is not listed or accessible and direct token access returns 403 Not Authorized And members within the same household see the method only if they have payment permission, with details masked And attempts to charge the token against a different account or household are blocked without processor submission And all denied access and blocked charge attempts are logged with actor, account, method identifier hash, and timestamp
Payment Confirmation, Receipts, and Audit Logging
"As a board treasurer, I want clear receipts and an audit trail for every payment so that our records stay accurate and compliant."
Description

After authorization/capture, issue an in-app and email receipt with wallet type or method used, amount, memo, due date, fees, and confirmation number. Post the receipt and status change back to the original feed item and ledger, and update compliance logs. Ingest processor webhooks for succeeded, failed, and disputed events, reconciling against the payment intent with idempotent updates. Log a complete audit trail of attempts (wallet launched, fallback used, declines, retries), device context, and actor identity with correlation IDs for support and compliance. Expose receipt and event history to board members and managers with appropriate permissions, and support export for accounting.

Acceptance Criteria
Successful Capture: In‑App and Email Receipt
Given a payment is authorized and captured for a community dues post When the processor confirms capture or a success webhook is received Then an in‑app receipt is created and visible to the payer within 5 seconds And an email receipt is queued within 10 seconds and the delivery attempt is logged with the provider message ID And both receipts include: confirmation number, amount, memo, due date, itemized fees, and wallet type or payment method used And the values on the receipt match the payment intent and processor amounts exactly And the receipt is associated with the payer account and the original dues post
Feed Item and Ledger Update on Status Change
Given an existing dues feed item with an associated payment intent When the payment status changes to succeeded, failed, or disputed Then the feed item displays the current status badge and a receipt/event entry is appended And for succeeded payments, a ledger entry is created with gross amount, fees, and net amount And for failed payments, no financial ledger entry is created and a failure event is logged And for disputed payments, a dispute adjustment entry reversing the net amount is created and linked to the original transaction And the receipt/entry references the confirmation number, payer, due date, memo, and correlation ID And compliance logs are updated with the status change, timestamp (UTC), and actor identity And all updates complete within 5 seconds of event processing
Idempotent Webhook Ingestion and Reconciliation
Given the processor sends webhook events for a payment_intent_id When duplicate or out‑of‑order succeeded, failed, or disputed events are received Then processing is idempotent using payment_intent_id and event_id such that ledger entries and receipt status are updated exactly once And the final payment state reflects the latest valid processor event according to processor state transitions And invalid or unsigned webhook events are rejected and do not mutate state And each processed webhook is recorded with correlation ID, timestamp (UTC), and prior→new state And replaying the same events results in no additional changes to ledger, feed, or compliance logs
End‑to‑End Audit Trail with Correlation IDs
Given a payer initiates payment via Wallet SmartOpen When the wallet launches, a fallback is used, a decline occurs, or a retry is attempted Then an audit entry is recorded for each attempt with: timestamp (UTC), actor identity, device type, OS version, browser/app version, IP address, and outcome (launched, fallback_used, declined, retried, succeeded, failed, disputed) And audit entries include a correlation ID linking attempts, receipt, ledger entry, and webhooks And audit entries are immutable (no edit/delete via API or UI) and retained for at least 24 months And authorized users can search audit entries by confirmation number, payment_intent_id, payer, or correlation ID and receive results within 2 seconds for up to 10k records
Role‑Based Access to Receipts and Event History
Given users with roles resident, board member, manager, and support admin When a user opens a payment detail view Then residents can view only their own receipts and event history; board members and managers can view receipts and events for their community; support admins can view all communities And unauthorized users receive HTTP 403 and no payment data is returned And payment method details are masked appropriately (wallet type shown; card/ACH masked to last4) And event history is shown in chronological order with pagination and filters by status and date And access controls are enforced consistently at API and UI layers
Accounting Export with Ledger Reconciliation
Given a board member or manager requests an accounting export for a date range and community When the export is generated Then a UTF‑8 CSV is produced within 60 seconds containing per‑transaction rows with columns: transaction_date (ISO 8601), confirmation_number, payer_name, payer_unit, amount, fees, net_amount, memo, due_date, method, status, dispute_status, ledger_account, correlation_id And the sum(amount) and sum(net_amount) in the export match the community ledger totals for the same filters with zero variance And the export includes only records the requester is authorized to view And the export is delivered via a time‑limited, signed URL and the export event is logged in compliance records with requester identity and correlation ID
Analytics, Feature Flags, and Rollout Controls
"As a community manager, I want to monitor and control Wallet SmartOpen and see conversion metrics so that I can improve on-time payments safely."
Description

Add admin controls to enable Wallet SmartOpen per community and segment by channel (feed, email, SMS). Provide a kill switch and rate limiting to mitigate issues. Track funnel metrics end-to-end (link open, wallet readiness, sheet shown, confirm, success, fail, fallback usage, time-to-pay) and expose them in a lightweight dashboard with export to analytics systems. Support A/B testing of routing rules and UI copy to improve conversion. Store metrics in privacy-conscious, aggregated form and honor user consent/preferences. Surface operational alerts for elevated failure rates or processor/webhook issues.

Acceptance Criteria
Enable SmartOpen Per Community and Channel
Given I am an Org Admin for Community X with Settings permission When I enable Wallet SmartOpen for channels Feed and Email and disable it for SMS Then new posts and notices sent via Feed and Email for Community X use SmartOpen links, and SMS uses the standard fallback link And the configuration change is persisted and propagated to delivery services within 60 seconds And an audit log entry is recorded with admin_id, community_id, channels_changed, old_value, new_value, and timestamp UTC
Global Kill Switch and Rate Limiting
Given the global Wallet SmartOpen kill switch is set to On When any SmartOpen link is generated or opened Then the link routes to the secure card/ACH screen or saved method (fallback) instead of opening a wallet And all funnel events for these sessions include flag = "killed_by_admin" And the change propagates within 60 seconds across all communities Given rate limiting is configured to 120 wallet inits per minute per community When the threshold is exceeded within a rolling 60-second window Then additional wallet inits are short-circuited to fallback with a "rate_limited" event containing the dropped count
End-to-End Funnel Event Capture
Given a recipient engages with a SmartOpen link from any channel When they progress through or exit the payment flow Then the system records events: link_open, wallet_ready, sheet_shown, confirm, success, fail, fallback_used And time_to_pay (milliseconds) is computed for successful payments from link_open to success And 99% of events are available in analytics within 5 minutes of occurrence And each event includes: community_id, channel, ab_variant, device_type, anonymous_session_id, timestamp_utc And end-to-end event loss is <0.5% over 24 hours as measured by server-side link_open vs downstream events
Dashboard Visualization and Export
Given I am an Org Admin with Analytics permission When I open the SmartOpen dashboard and filter by date range (up to 90 days), community, channel, and A/B variant Then I see funnel counts and conversion rates per step and overall And I see median and p95 time_to_pay And the dashboard loads within 2 seconds for 30 days of data at ≤1M events And I can export CSV for the current filter within 60 seconds for ≤100k rows with a documented schema And I can configure export to an external analytics destination (signed URL or webhook) and see delivery success/failure And users without Analytics permission are denied access
A/B Test Setup and Attribution
Given I create an experiment with variants A and B and a 50/50 traffic split When I start the experiment Then recipients are deterministically assigned by recipient_id and channel, with stable assignment for 30 days or until the experiment ends And all funnel events carry the assigned ab_variant And I can pause or stop the experiment; stopping freezes assignment and preserves reporting And the dashboard shows conversion lift (absolute and relative) per variant with 95% confidence intervals
Consent and Privacy Preservation
Given a recipient has opted out of analytics tracking or has not granted consent When they interact with a SmartOpen link Then no user-identifying event data is stored; only aggregated counters at community/channel/day level are incremented And no device identifiers, IP address, or exact timestamps are stored for that recipient And SmartOpen functionality proceeds; if community policy requires opt-in to operate, the flow uses fallback instead And data retention enforces ≤30 days for event-level pseudonymous data and ≤13 months for aggregated summaries And admins can process a deletion request that purges event-level data for a recipient within 30 days
Operational Alerts for Elevated Failures
Given the 5-minute rolling failure_rate = failures/(successes+failures) exceeds 5% for any community or 3% globally, or webhook error rate exceeds 1% When the threshold is exceeded for two consecutive windows Then an alert is sent to configured channels (email and Slack/webhook) within 2 minutes with context (community, channel, error type) And an alert banner appears in the dashboard with recent errors and a link to toggle the kill switch And alerts auto-resolve after three consecutive windows below threshold And all alerts are logged with timestamp, severity, and acknowledgement status

Live Balance Sync

Quicklinks always reflect the latest balance, credits, fees, and partials in real time. If someone pays from another channel or a late fee posts, the link updates before checkout, with clear options for “Minimum Due” or “Pay in Full.” Prevents over/underpayments and reduces back‑and‑forth.

Requirements

Real-time Ledger Fetch and Merge
"As a resident payer, I want my balance to reflect the latest credits, fees, and payments at checkout so that I don’t overpay or leave an unintended balance."
Description

Implement a low-latency service that retrieves and merges the latest ledger data across sources (Duesly’s internal ledger, payment processor webhooks, and banking/lockbox feeds) to compute an up-to-the-moment balance, including credits, fees, reversals, and partial payments. The service must support median response <500 ms and P95 <1200 ms for balance reads, provide idempotent merge operations with deterministic conflict resolution, and expose a versioned API for the checkout flow and quicklinks. It must handle eventual consistency by applying event ordering, sequence numbers, and correlation IDs, ensuring the computed balance is authoritative at the time of request. This enables quicklinks to reflect accurate amounts prior to checkout and prevents over/underpayments.

Acceptance Criteria
Quicklink pre-checkout reflects latest external payment
Given an account has an existing balance of $500 in the internal ledger and a $200 payment is received via processor webhook after the quicklink was generated When the user opens the quicklink and the checkout flow requests the balance Then the service fetches and merges internal ledger, processor events, and bank/lockbox data for that account within the request And the returned Pay in Full equals $300 and Minimum Due matches the account’s minimum policy And the response includes an asOf timestamp, a deterministic version, and components detailing the $200 credit with source=processor and correlationId
Balance read meets latency SLOs under peak load
Given a sustained load of 250 concurrent balance-read requests across at least 10 accounts with mixed ledger sizes (1–500 entries) When the service processes balance reads over a contiguous 5-minute window Then median latency is < 500 ms and 95th percentile latency is < 1200 ms measured at the API boundary (server-side) And 5xx error rate during the window is < 0.5% And at least 99% of responses include a version and asOf timestamp
Duplicate and out-of-order events merge idempotently with deterministic outcomes
Given three events for the same assessment: charge +$100 (seq=1, corr=A), payment -$60 (seq=2, corr=B), reversal -$100 (seq=3, corr=A) arriving in order [seq=2, seq=1, seq=3] with one duplicate of seq=2 When the merge is executed any number of times for the account within the consistency window Then the resulting balance reflects exactly one payment of -$60 and a net zero impact from the charge and its reversal And no duplicate postings exist in the merged components And repeated reads over the same event set return the same balance and version
Event ordering with sequence numbers produces authoritative snapshot at request time
Given events for an account arrive out of order across internal, processor, and bank sources with sequence numbers and correlation IDs When a balance read is requested at time T Then the service orders events by per-entity sequence, applies reversals after their originals, de-duplicates by correlationId, and computes a deterministic version tagged with T And the returned components are strictly ordered and sum exactly to the returned balance And no component older than the current watermark for its source is included
Versioned balance API supports backward-compatible reads
Given a client requests GET /balance with header Accept-Version: v1 When the service returns a response Then the response schema matches v1 contract including fields: balance, minimumDue, payInFull, currency, asOf, version, components[] And requesting an unsupported version (e.g., v0) returns 406 with error code unsupported_version and a list of supported versions And omitting Accept-Version defaults to the latest supported minor of v1 and the response includes that version
Merge includes all sources once with provenance
Given one internal ledger fee +$50, one bank/lockbox payment -$20 (corr=L1), and one processor payment -$30 (corr=P1) exist for the account When the balance is requested Then the merged response includes exactly three components with types [fee, payment, payment] and sources [internal, bank, processor] And the net balance impact is $0 And each component includes its correlationId and source without duplication
Concurrent reads return monotonically non-decreasing versions
Given two clients request the balance concurrently while a new fee +$25 posts between their requests When both responses are received Then each response includes a version and asOf timestamp, and the later response has a strictly greater version than the earlier And each response’s components sum exactly to its returned balance And at most one of the responses includes the new fee component
Multi-Channel Payment Reconciliation
"As a board treasurer, I want off-platform payments to reconcile instantly to member accounts so that quicklinks show the correct amount and residents aren’t charged twice."
Description

Automatically reconcile payments received through external channels (e.g., ACH push, bank lockbox, in-office POS, third-party portals) to the correct household account and update the outstanding balance presented in quicklinks. Support ingestion via webhooks, SFTP files, and manual admin entry with deduplication using idempotency keys and matching rules (amount, date, reference, remitter). Model settlement states (authorized, pending, settled, reversed, chargeback) and reflect them in the balance calculation with clear business rules. Ensure reconciliation completes within minutes and triggers immediate balance refresh for any active quicklink sessions.

Acceptance Criteria
Webhook ACH Push Reconciliation Updates Quicklink Before Checkout
Given an external ACH push webhook with {idempotency_key, amount, date, reference, remitter} matching a single household When the webhook is received Then the payment is reconciled to that household and a ledger entry is created within 2 minutes p95 and 5 minutes p99 of receipt And then the settlement state from the payload is recorded (authorized, pending, or settled) and the balance is recalculated per settlement-state rules And then any active quicklink session for that household receives an in-session balance refresh within 3 seconds p95 of reconciliation and before the checkout step renders And then the quicklink displays updated “Minimum Due” and “Pay in Full” amounts that prevent over/underpayment relative to the recalculated balance And then processing is idempotent: resending the same webhook (same idempotency_key) does not change the ledger or balance
SFTP Lockbox File Ingestion with Idempotent Deduplication
Given an SFTP lockbox file containing N payment rows, including duplicates (same idempotency_key or composite match on amount+date+reference+remitter within a 5-day window) When the file is ingested Then only unique payments are reconciled and duplicate rows are ignored without affecting balances And then ingestion is safe to re-run: reprocessing the same file produces no additional ledger entries And then each unique payment is matched to a single household using matching rules with ≥98% auto-match rate on clean data; unmatched or multi-match rows are placed in a review queue And then reconciled rows update balances within 5 minutes p99 of file availability and emit an audit record including source filename and correlation id
Manual Admin Entry With Matching Rules and Conflict Resolution
Given an admin creates a manual payment entry with amount, date, reference, and remitter When they attempt to save Then the system proposes candidate households ranked by match score and requires explicit selection if multiple candidates exist And then saving generates an idempotency_key and creates a pending or settled ledger entry per admin selection, updating the balance per settlement-state rules And then if a subsequent external payment arrives with the same idempotency_key or composite match, the system de-duplicates by linking to the existing ledger entry without double-counting and records the source crossover in audit And then manual entries trigger the same quicklink balance refresh and reach active sessions within 3 seconds p95 of save
Settlement State Modeling Impacts Balance Calculation
Given a reconciled payment can be in states authorized, pending, settled, reversed, or chargeback When the balance is calculated Then authorized or pending payments reduce the “Pay in Full” and “Minimum Due” amounts by the pending amount and are labeled as Pending And then settled payments reduce balance by the settled amount And then reversed or chargeback increases balance by the reversed amount and applies any configured fees And then state transitions are atomic and visible in the balance within 2 minutes p95 of the event And then all transitions record an audit trail with prior_state, new_state, timestamp, actor, and source
Real-Time Session Refresh and Stale-Amount Guardrails
Given a user has an active quicklink session open for household H When an external payment or fee posts that changes H’s balance Then the session receives a push update and the displayed “Minimum Due” and “Pay in Full” values update without page reload within 3 seconds p95 of reconciliation And then if the user attempts to confirm payment with a stale amount, the server revalidates totals, blocks submission, and returns updated amounts And then over/underpayments are prevented by disallowing amounts outside [Minimum Due, Pay in Full] unless an explicit admin-configured override is present
Reconciliation Performance, Observability, and Error Handling
Given normal operating volumes up to 10,000 external payments per hour with bursty arrivals When reconciliation runs across webhook, SFTP, and manual ingestion Then end-to-end time from receipt/availability to balance update meets p95 ≤ 2 minutes and p99 ≤ 5 minutes And then system emits metrics for throughput, latency, match rate, dedup rate, and error rate with per-source dimensions, and alerts when any SLO is breached for ≥5 consecutive minutes And then transient ingestion errors are retried with exponential backoff up to 3 attempts and dead-lettered with actionable error context; balances are not altered for failed items
Pre-Checkout Balance Confirmation UI
"As a resident payer, I want to see a clear breakdown with Minimum Due and Pay in Full options so that I can choose the right amount confidently before paying."
Description

Provide a pre-checkout step for quicklinks that displays the current timestamped balance, line-item breakdown (dues, assessments, late fees, credits), and selectable payment options: “Minimum Due,” “Pay in Full,” and “Custom Amount” with guardrails (cannot be below minimum due or exceed policy-defined caps). Show a real-time notice if the balance changes during the session and re-calculate options accordingly. Include clear copy for how partial payments are applied and any pending fees that may post after settlement. This UI integrates with the balance API and blocks submission until a final revalidation passes.

Acceptance Criteria
Pre‑Checkout Balance Header and Timestamp Display
Given a valid quicklink and a responsive Balance API, When the pre‑checkout step loads, Then the UI displays the current balance formatted in account currency to two decimal places with thousands separators, And the Last updated timestamp matches the timestamp in the latest Balance API response and includes timezone information. Given the Balance API returns current_balance, When the UI renders, Then the displayed balance equals current_balance exactly to the cent. Given the Balance API is unavailable or returns an error, When the pre‑checkout step loads, Then the UI shows a blocking error state and disables the Continue/Pay action.
Line‑Item Breakdown Accuracy and Summation
Given the Balance API returns line items for dues, assessments, late fees, and credits, When the pre‑checkout step renders, Then each returned line item is displayed with its label and amount, credits display as negative amounts, and line items subtotal equals the algebraic sum of items. Given the Balance API returns current_balance, When the line‑item subtotal is calculated, Then the subtotal equals current_balance to the cent.
Preset Payment Options: Minimum Due and Pay in Full
Given current_balance > 0, When options are displayed, Then Minimum Due shows an amount equal to minimum_due from the Balance API and Pay in Full shows an amount equal to current_balance. Given the user selects Minimum Due or Pay in Full, When the selection is made, Then the payable amount equals the option amount and the Continue/Pay action is enabled. Given current_balance <= 0, When options are displayed, Then payment options are disabled and the UI indicates that no payment is due.
Custom Amount Guardrails and Error Handling
Given the user selects Custom Amount, When the entered amount is less than minimum_due, Then an inline validation message is shown and the Continue/Pay action is disabled. Given the user selects Custom Amount, When the entered amount exceeds the policy‑defined maximum payment cap, Then an inline validation message is shown and the Continue/Pay action is disabled. Given the user selects Custom Amount, When the entered amount is numeric, has at most two decimal places, and is within [minimum_due, policy cap], Then the Continue/Pay action is enabled and the payable amount equals the entered value.
Real‑Time Balance Change Notice and Recalculation
Given the pre‑checkout step is open, When the Balance API indicates a new timestamp with a different current_balance, Then a notice banner appears within 2 seconds stating the balance changed, the Last updated timestamp updates, and option amounts are recalculated to the new values. Given the user's current selection becomes invalid due to the change, When recalculation completes, Then the selection is marked invalid, the Continue/Pay action is disabled, and the user is prompted to reselect or adjust the amount.
Partial Payment Application and Pending Fees Copy
Given the pre‑checkout step is displayed, When the selected payable amount is less than current_balance, Then an info panel is visible describing how partial payments are applied as defined by policy and includes a disclaimer that pending fees may post after settlement. Given policy text and settlement window are provided by configuration, When the info panel renders, Then it displays the policy‑provided text verbatim and includes the settlement timing window; otherwise it displays default Duesly copy. Given the info panel is present, When the user changes the selection to Pay in Full, Then the info panel is hidden.
Final Balance Revalidation Blocks Submission
Given any payment option is selected, When the user clicks Continue/Pay, Then the UI performs a final revalidation call to the Balance API and blocks submission to the payment processor until revalidation succeeds. Given the revalidation response contains a different current_balance or a newer timestamp than what is displayed, When the user attempts submission, Then the UI cancels submission, updates the amounts and timestamp, shows a notice that the balance changed, and requires the user to reconfirm before proceeding. Given the revalidation call fails or times out, When the user attempts submission, Then submission is prevented, a retry control is shown, and no payment request is created.
Tokenized Quicklink Security and Permissions
"As a community admin, I want quicklinks to be secure and revocable so that only the intended resident can view their balance and initiate a payment."
Description

Secure quicklinks with signed, expiring tokens bound to the household/account and scoped to read balance and initiate a single payment session. Support configurable expiration, one-time or limited-use tokens, revocation/rotation by admins, rate limiting, replay protection, and device fingerprint checks. Enforce least-privilege permissions and ensure tokens cannot be used to access unrelated accounts or sensitive admin data. Record all access attempts and actions for compliance and incident response. This protects payers and communities while enabling frictionless access to real-time balances.

Acceptance Criteria
Signed Token Binding and Expiry
Given an active signing key with kid=K and a token T issued for account A When T is presented to the quicklink API Then the token signature verifies against the active key set and the alg matches the allowlist And the token payload contains accountId=A and scopes limited to balance.read and payment.session:create And the token contains exp within the configured TTL window and iat within allowable clock skew When T is used to request data for account B != A Then the API returns 403 UnauthorizedAccount and no account data is returned When the current time exceeds exp (plus allowable skew) Then all requests using T return 401 TokenExpired and are denied
Scoped Single-Use/Limited-Use Payment Tokens
Given a token T with allowed_uses=1 and scopes balance.read, payment.session:create When the balance endpoint is called with T Then the request succeeds and allowed_uses is not decremented When a payment session is created with T Then the session is created and allowed_uses decrements to 0 and the token state is marked consumed When any operation is attempted with T after allowed_uses=0 Then the API returns 409 TokenConsumed and no further actions succeed Given a token U with allowed_uses=3 When three payment sessions are created with U Then a fourth creation attempt returns 409 TokenConsumed and no new session is created
Admin Revocation and Key Rotation
Given an active token T When an admin revokes T via the dashboard or API Then subsequent uses of T are rejected with 401 TokenRevoked within 60 seconds of revocation And a revocation event with adminId, tokenId, accountId, and reason is recorded in the audit log Given a key rotation that promotes a new signing key K2 and schedules K1 deactivation When new tokens are issued after rotation Then they are signed with K2 and validate successfully And tokens signed with K1 continue to validate until the scheduled deactivation time When the deactivation time for K1 is reached Then tokens signed by K1 are rejected with 401 TokenSignatureInvalid and the JWKS reflects the removal of K1
Rate Limiting and Replay Protection
Given a token T and client IP X When more than 10 requests per minute per token or IP are made to balance or payment session endpoints Then additional requests within that minute are rejected with 429 TooManyRequests and include a Retry-After header Given a payment session create request with idempotency-key=IK When the same IK is submitted again within 24 hours Then no duplicate session is created and the API returns the original result with 409 IdempotencyConflict Given a consumed token T (allowed_uses=0) When T is presented again to create a payment session Then the attempt is treated as a replay and rejected with 409 TokenConsumed and logged as replay_attempt=true
Device Fingerprint Verification
Given a new token T on first successful use When the device fingerprint F (derived from user agent, platform, and first-party cookie) is captured and bound to T Then subsequent requests with T must present the same fingerprint F When a request presents a different fingerprint F' Then the API rejects the request with 401 FingerprintMismatch, marks T as suspected, and logs the event When the fingerprint is missing (e.g., cookies cleared) Then the API rejects the request with 401 FingerprintMissing and does not disclose any account data
Least-Privilege Access and Data Isolation
Given a token scoped to balance.read and payment.session:create When the token is used to call any admin or configuration endpoint Then the API returns 403 Forbidden and no side effects occur When the token is used to request data for a different accountId, household, or community Then the API returns 403 UnauthorizedAccount and returns no PII Then balance responses include only: balance_due, credits, fees, partials, minimum_due, pay_in_full_options, currency, and timestamps, with no contact info or admin metadata
Audit Logging and Incident Traceability
Given any access attempt (success or failure) using a quicklink token When the request is processed Then an immutable audit record is written with timestamp (UTC), tokenId/jti, accountId, communityId, action, outcome, reason_code, IP, user_agent_hash, device_fingerprint_hash, request_id, and adminId (if applicable) And audit records are queryable by tokenId, accountId, action, and time range within 5 seconds for up to 10,000 records And audit records are retained for at least 24 months and protected against tampering (WORM or hash-chained) When an incident response report is generated for a tokenId over a defined window Then the system produces a chronological sequence of all related events with correlated request_ids and no gaps
Sync Resilience and Fallback Strategy
"As a resident payer, I want the system to handle outages gracefully and revalidate at submission so that my payment amount is correct even if some services are slow."
Description

Implement resilient sync behavior including exponential retries, circuit breakers, and stale-while-revalidate caching when upstreams (processors, banks) are slow or unavailable. Define cache TTLs and freshness indicators in the UI, and present a safe fallback (e.g., last-known minimum due) with explicit messaging. On submit, perform atomic server-side revalidation of the amount and options before authorization and adjust the payment request if needed, prompting the user only when policy requires. Emit metrics and alerts on degraded sync, and ensure no payment proceeds without final revalidation.

Acceptance Criteria
Exponential Retry with Jitter on Upstream Fetch
Given balance fetches to upstream processors/banks experience timeouts or transient 5xx errors When requesting latest balances for a Quicklink open or refresh Then the system performs exponential backoff with full jitter: initial_delay=200ms, factor=2, max_retries=5, per_attempt_timeout=5s, max_total_wall_time=30s And retries cease immediately upon first successful response And failing after max_retries marks the attempt failed and increments metric=retry_failed with labels {endpoint, processor, community_id} And successful retry attempts record metric=retry_success with retry_count
Circuit Breaker Thresholds and Half-Open Recovery
Given consecutive upstream failures or timeouts during balance fetches When the rolling window of the last 20 requests within 60s shows a failure rate >= 50% Then the circuit breaker transitions to Open for 60s and short-circuits further fetches to cache fallback with reason=circuit_open And after cooldown, the breaker enters Half-Open allowing 5 trial requests; if success rate >= 80%, transition to Closed; otherwise return to Open for another 60s And circuit state transitions emit metrics {state, processor, endpoint} and are audit-logged
Stale-While-Revalidate Cache and UI Freshness Indicator
Given a user opens a payment Quicklink that requires balance display When a cached balance exists with age <= 60s Then show amounts from cache, label freshness='Updated just now' with last_updated timestamp, and start background revalidation And if cache age > 60s and <= 5m, label 'Checking for updates… Using last-known amounts' while revalidating; update UI on success And if cache age > 5m or no cache, attempt immediate fetch; on failure, fall back to last-known with banner='Out of date' and offer 'Retry Sync' And cache policies are configurable with defaults: fresh_ttl_seconds=60, swr_window_seconds=300
Safe Fallback Amounts and Explicit Messaging
Given the system is in fallback mode due to circuit_open or fetch failure When presenting payment options Then display 'Minimum Due' and 'Pay in Full' computed from last-known ledger amounts with visible message 'Using last-known amounts. We’ll recheck before charging.' and last_updated timestamp And prevent selection of amounts that would exceed outstanding by more than tolerance=max($1, 1%) and block negative/zero payments unless policy allows custom partials And provide a 'Retry Sync' action that triggers immediate revalidation and updates options if newer data is available
Atomic Revalidation at Submit with Policy-Driven Adjustments
Given a payer confirms 'Minimum Due' or 'Pay in Full' on the checkout screen When the server receives the payment request with idempotency_key Then the server atomically revalidates the selected option and amount against the authoritative ledger before creating any authorization And if |latest_amount - selected_amount| <= max_auto_adjust_delta where default=max($1, 1%), auto-adjust to latest and proceed without additional prompt; include adjustment details in response and audit_log And if delta exceeds threshold or the option becomes invalid, return 409 with new valid options and require user confirmation before proceeding And if revalidation cannot complete (e.g., upstream unavailable), return 503 and do not create or capture any authorization And idempotency guarantees no duplicate charges on client retries of the same request
Metrics, Alerts, and Prevention of Silent Degradation
Given normal and degraded sync operations over time When retries, circuit breaker events, cache hits/misses, stale fallbacks, revalidation adjustments, and revalidation blocks occur Then emit metrics with dimensions {community_id, processor, endpoint, outcome, reason}; provide dashboards for P50/P95 latency, cache_hit_rate, stale_fallback_ratio And trigger alerts when any 5m rolling window exceeds thresholds: circuit_open_time>120s, stale_fallback_ratio>0.2, retry_failed_count>50, revalidation_block_rate>0.05 And enforce that no payment proceeds to authorization or capture state without a recorded successful final_revalidation event
Balance Change Audit Log and Notifications
"As a board treasurer, I want a detailed balance change history and session alerts so that I can resolve disputes quickly and maintain trust with residents."
Description

Create an immutable audit log capturing every balance-affecting event with before/after amounts, source system, actor, timestamp, and correlation ID. Provide filtering and export in the admin UI and an API endpoint for support workflows. When a balance changes during an active quicklink session, display a non-intrusive in-session alert and optionally notify the resident via email/SMS per community policy. This transparency reduces disputes, supports compliance, and speeds support resolution.

Acceptance Criteria
Audit Log Entry on Balance Change
Given a balance-affecting event (external payment, late fee, manual adjustment) is committed for an account When the transaction is saved Then exactly one audit record is appended for the event And the record includes: account_id, ledger_id (if applicable), event_type, source_system, actor_id, actor_type, before_amount_minor, delta_minor, after_amount_minor, currency, timestamp_utc (ISO 8601, millisecond precision), correlation_id (UUIDv4) And before_amount_minor + delta_minor = after_amount_minor; values are stored as integers in minor currency units; decreases use negative deltas, increases use positive deltas And the audit record write completes within 1 second of the event commit and before any downstream notifications are sent
Immutability and Tamper Protection
Given any existing audit record When a user or service attempts to update or delete it via UI or API Then the request returns 405 Method Not Allowed and no record is changed And the attempt is logged as a separate security event with actor, timestamp, and target_record_id And the persistence layer enforces append-only semantics; direct update/delete attempts are blocked and return an error And audit records expose record_id and created_at; no fields are editable post-write
Admin UI Filter and Pagination
Given an admin opens the Audit Log UI When they filter by date range, account_id, event_type, source_system, actor_id, correlation_id, or amount range Then only records matching all applied filters are returned And results are sorted by timestamp_utc descending by default and can be toggled to ascending And pagination returns 50 records per page by default with cursor-based navigation; navigating pages preserves filters and sort And for a dataset with 10,000 records matching filters, the initial page loads within 2 seconds at p95
Audit Log Export (CSV)
Given filters are applied in the Audit Log UI When the admin clicks Export CSV Then a CSV is generated for the filtered result set with a header row and columns: record_id, account_id, ledger_id, event_type, source_system, actor_id, actor_type, before_amount_minor, delta_minor, after_amount_minor, currency, timestamp_utc, correlation_id And the exported row count equals the filtered count at export time And if the result set exceeds 100,000 rows, the export is delivered as a ZIP containing chunked CSV files of 50,000 rows each And numeric amounts are exported as integers; timestamps are UTC ISO 8601; encoding is UTF-8; line endings are LF And for up to 100,000 rows, the export completes within 30 seconds at p95 and is available via a time-limited download link (24-hour expiry)
Audit Log API for Support Workflows
Given a support client with audit.read scope When it calls GET /api/v1/audit/balances with filters (account_id, correlation_id, from, to, event_type, source_system, actor_id, min_amount, max_amount, limit, cursor) Then the API returns 200 with a paginated list that matches filters and conforms to the documented schema And unauthorized or insufficient scope requests return 401 or 403 respectively And the response includes next_cursor when more results exist; limit is capped at 500 And requests are rate-limited to 60 requests per minute per token; exceeding the limit returns 429 with Retry-After And p95 latency is ≤ 500 ms for queries returning ≤ 500 records on datasets up to 1,000,000 records And GET /api/v1/audit/balances/{record_id} returns the specific record (200) or 404 if not found
In-Session Quicklink Balance Change Alert
Given a resident is on a quicklink checkout page with an active session And the account balance changes on the server When the change is detected client-side Then within 3 seconds a non-intrusive alert is displayed indicating the balance was updated, including previous and new totals and event_type/reason And the Minimum Due and Pay in Full options update to reflect the new balance before payment can be submitted And if a custom amount is now below the new Minimum Due, the Pay action is disabled with guidance until adjusted And if a custom amount exceeds the new Pay in Full, the resident is prompted to confirm or adjust before submitting And the alert is accessible (ARIA live polite), does not steal focus, is keyboard navigable, and is dismissible And server-side revalidation prevents duplicate or stale-amount charges during submission
Resident Notifications per Community Policy
Given a community has configured notification policy for balance changes (email enabled/disabled, SMS enabled/disabled, quiet hours) When a balance-changing event posts to an account Then notifications are sent only via enabled channels and suppressed for disabled ones And deliveries occur within 2 minutes outside quiet hours; within quiet hours, notifications are queued and sent when the window opens And the message content includes community name, recipient name, unit/address, event_type, before and after balances, delta, and a secure quicklink URL And resident opt-outs are respected and no message is sent to opted-out channels And delivery outcomes (sent, failed, deferred) are recorded with correlation_id linking back to the originating audit record And if all channels fail, the system retries up to 3 times over 30 minutes and surfaces an admin-visible failure

Split Quicklink

Send separate quicklinks to co‑payers (spouse, tenant, roommate) with a defined split or pay‑what‑you‑can targets. Each link shows the payer’s portion and the live remaining balance; receipts are individual while the unit ledger stays unified. Easier coordination and faster collection without exposing private info.

Requirements

Split Quicklink Creation & Delivery
"As a community manager, I want to create and send split payment links with defined shares to multiple co‑payers so that I can collect dues faster without manual coordination."
Description

Enable managers to generate split payment quicklinks from any bill or announcement with one click, define co‑payers, and assign shares by fixed amounts, percentages, or pay‑what‑you‑can targets. Validate totals (including rounding) against the invoice, set due dates, optional caps/minimums, and link expiration. Create unique, signed tokens per co‑payer and deliver links via email/SMS or copy‑to‑clipboard, with resend and revoke controls. Display link status (sent, opened, paid) in the bill’s activity feed and log all actions for audit. Integrate with existing Duesly billing so the unit ledger remains the source of truth while links act as scoped payment intents.

Acceptance Criteria
One-Click Split Quicklink from Bill or Announcement
Given a bill or announcement with an invoice amount > $0 and a unit context When the manager clicks "Split Quicklink" Then the split composer opens prefilled with the invoice amount, unit, and bill reference within 2 seconds Given a bill or announcement without an invoice amount When the manager clicks "Split Quicklink" Then the action is disabled with an explanatory tooltip or the manager is prompted to enter an amount before proceeding Given the split composer with at least one valid co-payer and a balanced split per validation rules When the manager clicks Save Then a split set is created and the manager sees a confirmation and the bill activity feed entry "Split links created"
Co-Payer Definition and Share Assignment
Given the split composer When the manager adds a co-payer by email and/or phone Then formats are validated, duplicates are prevented, and invalid contacts display inline errors Given fixed-amount shares When the sum equals the invoice amount (or is <= the invoice if at least one pay-what-you-can link is present) Then the composer shows Balanced and Save is enabled Given percentage shares When totals equal 100% Then per-co-payer dollar amounts are auto-calculated to the cent per rounding rules and are read-only unless converted to fixed Given pay-what-you-can targets When a target is set Then the link displays "Target $X.XX" and payments are allowed up to the lesser of target, cap (if set), and remaining balance Given a mix of fixed, percentage, and pay-what-you-can shares When totals validation passes Then Save is enabled; otherwise Save is disabled with inline error messaging
Split Total Validation and Rounding
Given percentage shares producing fractional cents When amounts are rounded Then a deterministic remainder rule assigns leftover cents to the largest share (ties broken by earliest-added co-payer) Given the sum of fixed + rounded percentage amounts > the invoice total When attempting to Save Then Save is blocked and an error shows the overage amount Given the sum of fixed + rounded percentage amounts < the invoice total and no pay-what-you-can links exist When attempting to Save Then Save is blocked and an error shows the unassigned remainder amount Given at least one pay-what-you-can link and fixed + rounded percentage amounts <= the invoice total When attempting to Save Then the remainder is marked "to be covered by pay-what-you-can" and Save is allowed
Due Dates, Caps/Minimums, and Link Expiration
Given the split composer When the manager sets a due date for the split set Then each link shows the due date and reminders are scheduled against that date Given a co-payer link with a minimum and/or cap When the payer attempts a payment Then the amount must be >= minimum and <= the lesser of the cap and remaining balance or the payment is blocked with a clear message Given a link expiration datetime When the link is visited after expiration or after the invoice is paid in full Then the link shows "Expired" and cannot accept payment Given a link with no explicit expiration When the invoice is paid in full Then all outstanding links auto-expire within 10 seconds and display "Paid in full" on refresh
Unique Signed Tokens and Scoped Payment Intents
Given a generated split link When inspected Then it contains a unique, non-guessable, signed token scoped to the co-payer and the specific bill Given a request using a revoked or tampered token When the link is opened or payment is attempted Then access is denied with 403 and no bill details are revealed Given a valid token When a payment is completed Then the payment intent applies only to the associated unit ledger entry and the token cannot be reused Given an authenticated manager versus an anonymous co-payer When accessing the link Then manager-only data (other co-payers, internal notes, ledger history) is hidden from the co-payer
Multi-Channel Delivery, Resend, and Revoke
Given selected delivery channels (email, SMS) When the manager clicks Send Then each co-payer receives a unique link via the chosen channels and the bill activity feed records Sent per channel with timestamp Given Copy link for a co-payer When clicked Then the link is copied to clipboard and an activity event "Link copied" is logged without sending a message Given a previously sent link When the manager clicks Resend Then a new message is sent with the same token and the activity feed increments and timestamps the resend count Given a link When the manager clicks Revoke Then the token is immediately invalidated; further opens/payments are blocked; the activity feed records Revoked with actor and reason
Status Tracking, Live Balance, Receipts, Privacy, and Ledger Integrity
Given sent links When a co-payer opens a link Then the status changes to Opened and is visible in the bill activity feed with timestamp and channel, without exposing other co-payers’ identities or shares Given any co-payer payment When it succeeds Then the payer sees an on-screen receipt and receives an email/SMS receipt for the exact paid amount; the unit ledger records a single payment entry attributed to that payer; the link status becomes Paid Given multiple co-payers paying concurrently When aggregate payments would exceed the remaining invoice balance Then the system prevents over-collection by capping the last payment to remaining balance for pay-what-you-can or declining with a clear error for fixed shares; no ledger overage occurs Given a successful payment by any co-payer When another co-payer views their link Then the remaining balance and their payable portion update within 10 seconds Given the bill view in Duesly When split links are used Then the unit ledger remains the source of truth; no new invoices are created; all actions (create, send, open, pay, resend, revoke, expire) are logged with actor, timestamp, and channel and are exportable for audit
Live Remaining Balance Sync
"As a co‑payer, I want to see my share and the live remaining balance as others pay so that I know exactly what I owe and avoid overpaying."
Description

Show each co‑payer a real‑time view of their portion, what they have paid, and the live remaining balance for the unit without exposing other payers’ identities. Update amounts instantly as payments post via websockets or efficient polling, with concurrency controls that prevent overpayment and ensure atomic ledger updates. Reflect partial payments, handle simultaneous checkouts idempotently, and display post‑payment confirmation with the updated remaining balance. All updates post back to the unified unit ledger to keep accounting accurate.

Acceptance Criteria
Real‑Time Balance Update on External Payment
Given a payer has an open split quicklink displaying their portion and remaining unit balance When another co‑payer completes a payment that is posted to the unit ledger Then the open quicklink updates the remaining balance within 2 seconds of ledger post without requiring a page refresh And the delta equals the other payment amount (rounded per currency rules) and the payer's own portion‑to‑go is recalculated accordingly And no identifying info (name, email, payment method details) of other payers is displayed And an "Updated" timestamp reflects the latest sync within the last 2 seconds And if the websocket is unavailable or disconnects, the client falls back to polling every 5 seconds (±10% jitter) and still reflects the new balance within 5 seconds of ledger post
Concurrency Control During Simultaneous Checkouts
Given two or more co‑payers initiate checkout against the same unit remaining balance When their payment captures reach the backend at overlapping times Then ledger updates are atomic and serialized so the unit balance never drops below $0.00 And any capture that would exceed the remaining balance is auto‑adjusted to the exact remaining amount or declined with a clear message, and no over‑collection occurs And only one ledger entry per successful capture is created; duplicates are not created And both clients receive immediate UI updates reflecting the final remaining balance and their own adjusted payable amount within 2 seconds
Idempotent Payment Retries and Double‑Submit Protection
Given a payer submits a payment and a network error or timeout occurs When the client retries with the same idempotency key within 24 hours Then only one charge is captured and exactly one ledger entry is created And subsequent retries return the same transaction reference and status without additional capture Given a payer double‑clicks the Pay button or refreshes during processing When the backend receives duplicate requests for the same idempotency key Then only the first request is processed and the UI shows a single confirmation and receipt And any pending UI state resolves to the single final payment outcome within 5 seconds
Privacy of Other Payers’ Information
Given a payer is viewing their split quicklink When the page renders or live sync events arrive Then the UI does not display other payers’ names, emails, payment method details, or exact amounts paid And API responses used by the quicklink exclude other payers’ PII and expose only aggregate values (remaining unit balance, number of other payers) and the viewer’s own data And network payloads and page source do not contain fields with other payers’ PII And accessibility labels and alt text contain no PII of other payers
Atomic Unit Ledger Sync and Balance Consistency
Given a payment is successfully captured When the server writes to the unit ledger Then the ledger entry creation and remaining balance update occur atomically within a single transaction boundary And any GET of the unit ledger immediately after capture reflects both the new entry and the updated remaining balance in the same response And the sum of co‑payer payments for the unit equals the unit’s total collected amount within 1 cent tolerance according to currency rounding rules And audit logs include a trace ID linking the payment capture, ledger entry, and quicklink token
Post‑Payment Confirmation and Updated Link State
Given a payer completes a payment When the confirmation screen loads Then it displays the amount paid, the new remaining unit balance, and the payer’s cumulative paid‑to‑date for this bill And if the remaining balance is $0.00, the UI shows "Paid in full" and disables further payment actions on all related quicklinks within 2 seconds And if a partial balance remains, the quicklink shows the adjusted suggested amount equal to the payer’s remaining portion and prevents entry above the available remaining balance And reopening the same quicklink shows the updated state; attempting to replay the payment shows the existing receipt and performs no additional capture
Secure Tokenized Access & Privacy Controls
"As a community manager, I want split payment links to be secure and privacy‑preserving so that co‑payers can pay without logging in and without seeing each other’s information."
Description

Provide secure, signed, single‑use (or scoped multi‑use) quicklinks that allow guest checkout without login while protecting PII. Scope each link to a specific unit and invoice, hide other co‑payers’ identities and contact details, and show only the payer’s portion and the aggregate remaining balance. Support optional passcodes, link expiration, revocation, rate limiting, and anomaly detection for suspected link sharing. Align with PCI DSS by using hosted payment fields and minimize PII collection and retention. Record access events for compliance and audits.

Acceptance Criteria
Scoped Guest Access via Tokenized Quicklink
Given a valid signed quicklink scoped to Unit U and Invoice I with single-use=true, When the link is opened for the first time, Then the invoice details load without authentication and access is limited to Unit U and Invoice I. Given the same link after first successful view, When the link is opened again, Then the server returns 410 Gone and no invoice or unit data is returned. Given a valid signed quicklink scoped to Unit U and Invoice I with single-use=false and maxUses=3, When the link is opened 4 times, Then the 4th and subsequent requests return 410 Gone and are logged. Given any valid quicklink, When attempting to access any other unit, invoice, or feed endpoint, Then the server returns 403 Forbidden and no PII is included in the response.
PII Minimization and Co‑payer Privacy
Given a split invoice with co‑payers A, B, and C, When payer B opens their quicklink, Then only payer B’s portion amount and status are displayed and the identities and contact details of A and C are not displayed. Given the same page, When inspecting API responses, Then no names, emails, phone numbers, or identifiers of other co‑payers are present or derivable. Given guest checkout, When presenting input fields, Then only email for receipt is required, phone is optional, and no physical address is collected. Given payment completion by any co‑payer, When generating a receipt for payer B, Then the receipt contains only payer B’s payment details and a reference to the unified unit ledger without exposing other co‑payer information.
Display of Payer Portion and Remaining Balance
Given a split invoice, When a payer opens their quicklink, Then the UI shows their amount due and the live aggregate remaining balance for the unit and does not show other payers’ portions. Given another co‑payer pays part or all of their portion, When the current payer refreshes or the page polls within 5 seconds, Then the remaining balance updates to reflect the new total. Given a payment by the current payer, When the receipt is generated, Then it shows the amount paid by this payer, the new unit balance, and a unique receipt ID without revealing other payers’ names or amounts.
Passcode, Expiration, Revocation, and Messaging
Given a quicklink configured with a passcode, When opened, Then a passcode challenge is required before any invoice data is returned. Given a passcode-protected quicklink, When 5 incorrect passcode attempts occur within 1 hour from the same IP or device, Then the link is locked for 15 minutes and subsequent attempts receive 429 Too Many Requests with a Retry-After header. Given a quicklink with an expiration timestamp in the past, When opened, Then the server returns 410 Gone and the UI displays “Link expired” with a call-to-action to request a new link. Given a quicklink manually revoked by an admin, When opened within 60 seconds of revocation, Then the server returns 410 Gone and the access is logged with reason revoked.
Abuse Controls: Rate Limiting & Anomaly Detection
Given any quicklink, When requests exceed 60 opens per hour per link or 20 payment attempts per hour per IP/device, Then subsequent requests return 429 Too Many Requests with a Retry-After header and are logged. Given a single-use quicklink, When it is accessed from 3 or more distinct device fingerprints or 5 or more distinct /24 IP subnets within 24 hours, Then the link is flagged for suspected sharing, an admin alert is generated, and auto-revocation occurs if policy autoRevokeOnShare=true. Given an IP or device generating 10 or more failed passcode attempts across 3 links within 30 minutes, When further access is attempted, Then the IP/device is temporarily blocked for 30 minutes with 429 Too Many Requests.
PCI DSS Alignment and Hosted Payment Fields
Given the payment page, When entering card data, Then all PAN, expiry, and CVV fields are hosted by the payment processor via iframes and no PAN or CVV is sent to or stored on Duesly servers. Given network inspection, When submitting a payment, Then Duesly receives only a tokenized payment method reference and non-sensitive metadata, and stores only last4 and brand. Given ACH payment, When bank details are entered, Then collection occurs via processor-hosted elements and only a mandate or token reference is stored. Given data retention policy, When PII is stored for receipts or notifications, Then it follows a configurable retention period with a default of 24 months and is purged thereafter.
Access Event Logging and Auditability
Given any quicklink event (open, passcode success/fail, view, payment attempt/success, expiration, revocation, rate limit, anomaly flag), When it occurs, Then an append-only audit record is written with timestamp UTC, IP or IP hash, userAgent hash, device fingerprint hash, linkId, unitId, invoiceId, action, result, and HTTP status. Given an admin with audit permissions, When filtering logs by date range, unit, invoice, linkId, or IP, Then results are returned within 2 seconds for up to 10,000 events and export to CSV completes within 30 seconds for up to 100,000 events. Given retention enforcement, When events exceed the configured retention window, Then they are purged or archived and a system log entry confirms completion.
Individual Receipts with Unified Ledger Posting
"As a co‑payer, I want an individual receipt while the unit’s account stays accurate so that I can document my payment without affecting how the HOA tracks the total."
Description

Issue individual receipts to each co‑payer upon payment while posting consolidated entries to the unit ledger. Attribute payments to the co‑payer in metadata for internal reporting without exposing identities to other payers. Support partial payments, refunds, and chargebacks with correct proration back to the invoice. Deliver receipts via email/SMS with a confirmation number and downloadable PDF, and ensure exports and accounting integrations receive accurate, reconciled records tied to the original bill or announcement.

Acceptance Criteria
Individual Receipt Issuance per Co-Payer Payment
Given a unit invoice accessible via split quicklinks When a co-payer completes a successful payment via their unique quicklink Then the system generates a receipt addressed only to that co-payer with a unique confirmation number And the receipt includes: amount paid, currency, payment date/time (UTC), payment method brand and last 4, invoice/bill ID, unit ID, and quicklink ID And a PDF version of the receipt is generated that exactly matches the receipt content And the receipt and PDF are accessible to the paying co-payer and authorized admins, and are not visible to other co-payers And only one receipt is created per successful transaction; retries do not create duplicates (idempotent on processor transaction ID)
Unified Unit Ledger Posting and Reconciliation
Given an invoice receives one or more payments from multiple co-payers When those payments are posted Then the unit ledger records entries at the unit level tied to the original invoice/bill/announcement ID And payer identities are not shown on payer-facing ledger views; admin views may show internal payer references And the net of payments minus refunds/chargebacks on the ledger equals the sum of successful transactions for the invoice And the remaining balance equals invoice total minus net transactions with no rounding discrepancy greater than 0.01 in invoice currency
Internal Payer Metadata Attribution and Privacy Controls
Given a co-payer pays via a split quicklink When the payment record is stored Then the record includes internal metadata: co-payer reference (ID/token), quicklink ID, split amount/percentage at time of payment, and contact method And this metadata is available in admin reporting and internal exports only And other co-payers cannot see any direct identifiers (name, email, phone) of fellow co-payers in receipts, quicklink pages, or payer-facing exports And role-based access control restricts metadata visibility to authorized admins
Partial Payment Allocation and Receipt Accuracy
Given an invoice with multiple line items, taxes/fees, and discounts And a co-payer makes a partial payment less than the remaining balance When the payment is applied Then the system allocates the payment to the invoice per the configured allocation rule and records per-line allocations And the receipt displays amount paid and an allocation summary And the unit ledger and all active quicklinks reflect the updated remaining balance And no line item allocation exceeds its outstanding amount; rounding adjustments are less than or equal to 0.01 per line item and fully reconciled
Refunds and Chargebacks Proration and Reversals
Given prior payments exist on an invoice from one or more co-payers When a full or partial refund is issued or a chargeback is received Then the system creates reversal entries prorated back to the original allocations and links them to the original confirmation number and invoice ID And the unit ledger shows the reversal with correct sign and updates remaining balance accordingly And the impacted co-payer receives an individual refund receipt/notification; other co-payers are not notified and cannot infer the refunded payer’s identity And exports and integrations include a distinct reversal record linked to the original bill and transaction
Receipt Delivery via Email and SMS with PDF Download
Given a successful payment by a co-payer When the system sends the receipt Then an email is delivered if an email is on file and/or an SMS is delivered if a mobile number is on file, per quicklink contact preferences And each message includes the confirmation number, amount paid, and a secure time-limited link to download a PDF of the receipt And delivery status is tracked; on email bounce or SMS failure, the system retries once and logs the failure for admin review And the PDF link opens a valid PDF that exactly matches the receipt content in common PDF viewers
Accurate Export and Accounting Integration Records
Given an invoice paid by multiple co-payers including partials and a refund When exporting or syncing to accounting Then each payment/refund/chargeback produces a record tied to the original invoice/bill or announcement ID and the unit account And totals in the export/integration equal the unit ledger net for the invoice; no duplicates occur due to idempotent external IDs And required fields are present: transaction date, settlement date, signed amount, confirmation number, payment method, unit ID, invoice ID, and internal payer reference (internal-only in admin exports) And the integration posts transactions to the configured AR account/item mapping and passes reconciliation without errors
Automated Co‑payer Reminders & Tracking
"As a community manager, I want automated reminders to nudge only the co‑payers who haven’t paid their share so that I don’t have to chase people manually."
Description

Enable automated, configurable reminder cadences targeted to co‑payers who have not met their share, including pre‑due, due‑day, and past‑due nudges. Personalize content, include the live link, and suppress reminders when the unit is fully funded. Respect community communication preferences, opt‑outs, and quiet hours. Track delivery, opens, bounces, and conversions per co‑payer, and surface a reminder timeline in the bill’s activity feed with manager controls for manual re‑send or pause.

Acceptance Criteria
Pre‑due reminder cadence for unpaid co‑payers
Given a bill with split quicklinks and a configured pre‑due reminder schedule And at least one co‑payer’s paid amount is less than their assigned target When the pre‑due reminder window occurs Then reminders are sent only to co‑payers with an outstanding amount via the configured channel(s) And no reminder is sent to co‑payers who have fully met their target And each send is logged with bill ID, co‑payer ID, schedule type=pre‑due, channel, and timestamp
Due‑day and past‑due nudges until share is met
Given a bill with a defined due date and a configured past‑due cadence And a co‑payer’s paid amount is less than their assigned target at due‑time When the due‑time passes Then a due‑day reminder is sent to that co‑payer and logged When the bill becomes past‑due Then reminders are sent at the configured intervals until the co‑payer’s cumulative paid >= their target or the max attempts is reached And after the target is met, no further due/past‑due reminders are sent to that co‑payer
Communication preferences and opt‑outs enforcement
Given community‑level default channels and quiet hours And a co‑payer has channel preferences and/or has opted out of specific channels When an automated reminder is scheduled Then the system sends only via the co‑payer’s allowed channels And if all channels are opted‑out, the reminder is suppressed and the suppression is logged with reason=opt‑out And global/legal unsubscribe lists are respected for email, with no sends attempted to unsubscribed addresses
Quiet hours and local time zone adherence
Given quiet hours are configured at the community level And a co‑payer has a known local time zone (else use community time zone) When a reminder is due during quiet hours Then the send is deferred to the next allowable window outside quiet hours in the co‑payer’s local time And the scheduled and actual send times are recorded And manual re‑sends also respect quiet hours
Personalized reminder content with live split link
Given a co‑payer has an assigned target and current remaining amount When a reminder is generated Then the content includes the co‑payer’s name (if available), their assigned target, current remaining amount, due date, and a unique secure quicklink URL And the quicklink displays the live remaining balance at click time And the content does not reveal other co‑payers’ names, amounts, or contact details
Per co‑payer tracking: delivery, opens, bounces, conversions
Given a reminder is attempted for a co‑payer When the message is processed by the channel provider Then delivery status (queued, sent, delivered, failed/bounced) is captured and stored per co‑payer and channel And if supported by the channel, opens and link clicks are recorded And a conversion is recorded when the co‑payer’s cumulative paid >= their configured target, with attribution to the last clicked reminder when applicable And bounces suppress future sends for that channel until the address/number is updated, with the suppression reason logged
Manager controls: pause and manual re‑send in activity feed
Given a manager views the bill’s activity feed When the manager pauses reminders for a specific co‑payer Then no automated reminders are sent to that co‑payer while paused, and the pause event is logged with user, timestamp, and scope When the manager triggers a manual re‑send for a co‑payer Then the system queues an immediate reminder that respects preferences and quiet hours, logs the action, and updates the reminder timeline And the activity feed displays a chronological timeline of sends, suppressions, pauses/resumes, bounces, and conversions
Editable Splits & Reconciliation Rules
"As a community manager, I want to adjust splits and handle over/underpayments after sending links so that I can resolve real‑world changes without breaking reconciliation."
Description

Allow managers to edit splits after links are issued: add/remove co‑payers, adjust shares, and redistribute the remaining balance while preserving a versioned history. Lock amounts already paid, recalculate remaining portions, and automatically reissue or invalidate affected links. Define over/underpayment rules (cap at share, allow overage to reduce remaining, or convert to unit credit), rounding behavior, and closing logic when fully paid. Reopen flows cleanly on refund or dispute.

Acceptance Criteria
Checkout Integration & Fees Transparency
"As a co‑payer, I want a clear checkout with transparent fees and payment method choices so that I can pay quickly and confidently."
Description

Integrate split quicklinks with the existing Duesly checkout to support ACH, cards, and digital wallets in a mobile‑optimized, accessible flow. Clearly present the payer’s share, any applicable processing fees or surcharges, and the net amount applied before confirmation. Handle declines with inline retry, respect stored payment methods for logged‑in users while allowing guest checkout, and return the payer to a confirmation screen that reflects the updated remaining balance. Ensure compliance with regional receipt and disclosure requirements.

Acceptance Criteria
Display of Payer Share, Fees, and Net Amount
Given a split quicklink with a defined share or pay‑what‑you‑can target and applicable fees/surcharges When the checkout loads Then the payer sees separate line items for Amount, Fees/Surcharges, Total Charged, and Net Applied before confirmation Given the payer edits the amount within allowed bounds and not exceeding the remaining balance When the amount changes Then Fees/Surcharges, Total, and Net Applied recalculate instantly and are correctly formatted for the locale Given surcharges are not permitted in the payer’s region When the checkout loads Then no surcharge line is displayed and totals exclude surcharges
Inline Retry on Payment Failure
Given a payment attempt is declined (card, ACH, or wallet) When the gateway returns an error Then an inline error message appears mapped to the field, the Confirm button re‑enables, and no duplicate ledger entry is created Given a soft decline occurs When the payer retries with corrected details in the same session Then the retry processes without losing the selected amount and idempotency prevents duplicate charges on refresh/back Given a hard decline is detected When the error is returned Then the UI offers switching to an alternate method (e.g., ACH ↔ card ↔ wallet) without leaving the flow
Stored Payment Methods and Guest Checkout
Given the payer is logged in and has stored payment methods When the checkout loads Then masked stored methods are selectable by default and the payer can add a new method Given the payer arrives via quicklink unauthenticated When the checkout loads Then guest checkout is available without requiring account creation and all validations still apply Given a stored method is selected When payment is confirmed Then the tokenized method is used, required authentications are triggered, and the receipt is associated with the payer profile
Digital Wallets, Cards, ACH, and SCA/3DS Support
Given the browser/device supports Apple Pay or Google Pay and the page is served over HTTPS When the checkout loads Then eligible wallet buttons appear and initiate payment for the payer’s share with fees applied consistently Given card and ACH are enabled for the community When the payer selects a method Then method‑specific fields validate (format, routing/account, expiry/CVV) and display method‑specific fees/processing time Given a card transaction requires SCA/3DS When the issuer challenges Then a challenge flow opens inline and on success the checkout resumes to finalize payment; on failure, inline retry is offered
Accessibility and Mobile Optimization
Given a mobile device at ≤375px width on a typical 4G connection When the checkout loads Then content fits without horizontal scrolling and key actions are tappable (44×44px min); First Contentful Paint ≤2.0s Given keyboard‑only navigation or screen reader usage When traversing the form Then focus order is logical, all inputs have accessible names, amounts/fees/totals are announced correctly, and errors use ARIA‑live Given locale‑specific formatting When rendering currency values Then amounts use the payer’s locale/currency formatting consistently across form, summary, and confirmation
Confirmation, Remaining Balance Update, and Receipts Compliance
Given a successful payment When redirecting to confirmation Then the screen shows Amount, Fees/Surcharges, Total Charged, Net Applied, and the live updated remaining balance for the unit within 2 seconds Given regional receipt/disclosure rules apply When generating the receipt Then the receipt includes legal entity name, date/time with timezone, unique receipt ID, last4 or masked method, itemized fees/surcharges, net applied, and required regional disclosures; a copy is sent via email and accessible via link Given co‑payers have active quicklinks When one payment posts Then other quicklinks reflect the new remaining balance within 10 seconds without exposing the payer’s identity
Privacy and Data Minimization for Co‑Payers
Given a payer opens their split quicklink When viewing checkout details Then only their share/target, due date, and unit alias are shown; no other co‑payer names, contact details, or amounts are visible Given API requests and logs When processing the checkout Then requests are scoped by link token to the payer’s share and PII is masked in logs (e.g., card PAN masked, bank info truncated) Given the payer attempts to enter an amount exceeding the remaining balance When submitting Then the system caps the charge to the remaining balance and explains the cap without revealing other co‑payers’ payments

Link Rescue

Continuously monitors SMS deliverability and auto‑regenerates a fresh magic link if a message bounces or expires. Falls back to email/push and can escalate to a voice read‑out code for critical dues—respecting time zones and quiet hours. More members reached, fewer missed due dates.

Requirements

SMS Deliverability Monitoring
"As a board member, I want real-time visibility into whether members received our SMS dues notices so that I can ensure everyone is reached and take action if delivery fails."
Description

Continuously track SMS message status via provider webhooks (queued, sent, delivered, failed, filtered) and correlate each status to the originating invoice/announcement and member profile. Normalize carrier error codes, detect bounces/filters, and mark messages as undeliverable with reasons. Surface real-time metrics and per-member delivery state to the communication feed and admin dashboard. Persist message SIDs and signed webhook verification to prevent spoofing. This monitoring acts as the trigger for automated link regeneration and channel failover, ensuring timely dues collection and higher reach rates.

Acceptance Criteria
Signed Webhook Verification and SID Persistence
Given an inbound SMS status webhook contains provider signature headers and a MessageSid When the signature is verified against the configured signing secret Then the event is accepted and persisted with MessageSid, provider event type, provider timestamp, raw payload hash, and verification outcome And duplicate deliveries of the same webhook (same MessageSid, status, and provider timestamp) are ignored idempotently And webhooks with missing/invalid signatures return HTTP 401, are not persisted, and a security audit event is recorded
Status Normalization and Undeliverable Reasoning
Given a webhook with a provider status and optional error code/text When the event is processed Then the status is normalized to one of {Queued, Sent, Delivered, Undeliverable, Expired, Filtered} And any provider/carrier error code is mapped to a standardized undeliverable reason enum (e.g., SpamBlock, UnknownDestination, Landline, CarrierViolation, UserOptOut, Other) and stored alongside the raw code/text And messages with normalized states {Undeliverable, Expired, Filtered} are marked undeliverable with reason and timestamp on both the message and the member’s SMS contact record
Correlation to Invoice/Announcement and Member
Given an outbound SMS was created for invoice INV-123 and member M-456 with internal message_id MID-789 and provider MessageSid When any webhook for that MessageSid is received Then the delivery log for MID-789 is updated and linked to INV-123 and M-456 And the communication feed item for INV-123 reflects the latest normalized delivery state within 5 seconds of webhook receipt (p95 <= 5s) And the member profile shows the current SMS deliverability status and last attempt reason And an audit trail shows the chain: INV-123 -> MID-789 -> MessageSid -> latest state
Idempotent and Ordered State Updates
Given webhooks may arrive out of order or be duplicated When a webhook with an older provider timestamp than the currently stored latest event is received Then it is stored for audit but does not change the message’s latest delivery state And duplicate events (same MessageSid, status, and timestamp) do not create additional log entries or counters And state precedence rules apply: Sent supersedes Queued; Delivered and Undeliverable are terminal; Expired/Filtered set Undeliverable; no terminal state is downgraded by a non-terminal state
Real-time Metrics and Dashboard/Feed Visibility
Given live webhook processing is operating When status events are processed Then the admin dashboard metrics update aggregates by normalized state and undeliverable reason with end-to-end latency p50 <= 2s and p95 <= 10s And per-member delivery state badges in the communication feed refresh within 5 seconds (p95) without manual reload And an export/API endpoint exposes last 30 days of delivery metrics by community, message type (invoice/announcement), and channel
Trigger Emission for Link Regeneration and Channel Failover
Given a message becomes Undeliverable, Filtered, or Expired without a subsequent Delivered event within 15 minutes of initial send When that condition is met Then a rescue_requested event is emitted containing member_id, invoice_id/announcement_id, MessageSid, normalized reason, local member time zone, and quiet-hours window And the message record is flagged needs_rescue=true to prevent duplicate triggers And clearing or fulfillment of the rescue flow (e.g., later Delivered via fallback) marks needs_rescue=false and records the rescue outcome
Auto-Regenerate Magic Link & Invalidate Stale Links
"As a resident, I want any expired or failed link to be automatically replaced with a working one so that I can pay dues without contacting support."
Description

When an SMS bounce, filter, or expiration is detected, generate a fresh, signed, short-lived magic link tied to the specific invoice/post and member. Immediately invalidate previously issued links for that context to prevent confusion or misuse, maintaining idempotency of payments and acknowledgment actions. Configure link TTL, single-use behavior, and optional device binding. Update all relevant records and logs with the new link, and hand off to the failover orchestrator for re-dispatch through the next best channel.

Acceptance Criteria
SMS Bounce: Auto‑Regenerate and Invalidate Prior Links
Given an SMS delivery receipt for a message containing a magic link returns status "bounced" or "undeliverable" When the bounce event is processed Then a new signed, short‑lived magic link tied to the same member and invoice/post is generated within 3 seconds And all previously issued links for that member‑invoice context are marked invalidated within the same transaction And the new link record is persisted with fields: link_id, member_id, context_id (post_or_invoice_id), created_at, expires_at (TTL), single_use flag, device_binding flag, signature And an audit log event "link_regenerated" is written with bounce_reason, prior_link_ids, new_link_id, actor="system" And the new link is handed off to the failover orchestrator with correlation_id and channel_hints
Carrier Filter Detected: Auto‑Regenerate and Invalidate
Given an SMS provider status webhook indicates "filtered", "blocked", or "spam" for a magic link message When the filter event is received Then the system regenerates a new magic link within 3 seconds with the same member and context And all prior links for that context are invalidated atomically And any attempt to use a prior link returns HTTP 410 link_invalidated with no side effects And a structured metric and audit log are recorded with reason="filtered" and correlation_id
Link TTL Expired: Regenerate on Access Attempt
Given a member opens a magic link whose TTL has elapsed When the request is received Then the system returns HTTP 410 link_expired and does not execute payment or acknowledgment And a fresh magic link is generated within 3 seconds using the configured TTL and same context And the fresh link is persisted and the expired link remains invalidated And the fresh link is handed off to the failover orchestrator for re‑dispatch via the next best channel
Idempotent Payment/Ack: No Duplicate Effects Across Links
Given multiple links (old invalidated and newest active) exist for the same member‑invoice context And the member initiates payment or acknowledgment using any of them near‑simultaneously When the first action is successfully recorded Then exactly one payment/acknowledgment record exists for the context And subsequent attempts return a success with the existing receipt/ack reference and create no new transactions And the idempotency key derived from member_id + context_id is reused across all links And any invalidated link cannot produce side effects even if replayed
Single‑Use Enforcement and Optional Device Binding
Given single‑use is enabled for magic links When a generated link is first successfully redeemed Then the link is marked consumed and cannot be used again (HTTP 410 link_consumed) And if device binding is enabled, the first redemption binds the link to the device fingerprint; subsequent attempts from other devices return HTTP 403 device_mismatch And if device binding is disabled, redemption is permitted from any device while still enforcing single‑use And consumption and device binding details are logged with device_id, user_agent_hash, and ip_hash
Configurability: TTL, Single‑Use, and Device Binding
Given an admin configures magic link TTL, single‑use behavior, and device binding at the community or post/invoice level When the settings are saved Then new links reflect the configured TTL (allowed range 5 minutes to 30 days) and flags And changes are written to an audit log with who, when, old_value, new_value And per‑post/invoice overrides take precedence over community defaults And invalidation behavior remains consistent for already‑issued links despite subsequent configuration changes
Failover Handoff: Payload and Timing Guarantees
Given a regeneration occurs for any reason (bounce, filter, expiration) When the new link is created Then a handoff event is published to the failover orchestrator within 2 seconds with payload including member_id, context_id, new_link_url, expires_at, reason, correlation_id, time_zone, and quiet_hours_window And if publish fails, the system retries with exponential backoff up to 3 times without generating additional links And a failed handoff after retries raises an alert and is logged with error details
Multi-Channel Failover Orchestration
"As a part-time property manager, I want the system to automatically try alternate channels when SMS fails so that more members receive critical dues notices without manual intervention."
Description

Define and execute a rules-based sequence for message delivery across SMS, email, push notifications, and voice based on deliverability signals, user preferences, and message criticality. Deduplicate notifications to avoid spamming, honor per-channel cooldowns, and record outcomes per attempt. Include configurable delays and maximum attempts per channel. Automatically select the next channel when a failure or expiration occurs, and stop the sequence once a successful engagement (link opened or payment started) is detected.

Acceptance Criteria
SMS Bounce Triggers Email Failover
Given a dues notice magic link was sent via SMS for message_id X and the carrier returns a definitive bounce/undeliverable code within 2 minutes When the failure is recorded by the orchestrator Then the system regenerates a new magic link for X and sends an email to the member within 60 seconds using the preferred email template for X And Then no additional SMS attempts for X are made for that member until the SMS cooldown for X (configurable, default 30 minutes) elapses or until engagement is detected And Then the attempt is logged with channel=SMS, attempt_number incremented, status=failed, and carrier_reason_code captured; the follow-on email attempt is logged with channel=EMAIL, status=sent
Expired Link Auto-Regeneration and Next-Channel Switch
Given the last delivered channel for message_id X contained a magic link with TTL T minutes And Given no engagement was recorded before expiry When the link expires or an explicit expiration event is emitted Then the system regenerates a new magic link and selects the next channel in the failover sequence based on user preferences, deliverability score, and message criticality And Then the next-channel attempt is sent within 60 seconds of expiration if outside quiet hours; otherwise it is deferred to quiet-hours end And Then the previous link is invalidated and returns HTTP 410 within 5 seconds of regeneration
Quiet Hours and Time Zone Respect with Critical Escalation
Given a member’s time zone is known and quiet hours are configured (e.g., 21:00–08:00 local) When a failover attempt becomes due during quiet hours Then no SMS, email, push, or voice is sent until quiet hours end And Given the message criticality is "critical dues" When quiet hours end Then the next channel in sequence is executed within 5 minutes, preferring push/email before SMS, and voice read‑out code is used only if all other channels fail or are unavailable And Then the voice call, when used, reads a one-time code linked to the regenerated magic link; the code expires in 15 minutes and is redacted in logs
Deduplication and Cooldown Across Channels
Given message_id X is in-flight for a member When multiple channels are concurrently eligible per rules Then the orchestrator sends only one channel attempt at a time and suppresses other channels for a suppression window (configurable, default 90 seconds) And Then per-channel cooldowns are enforced: the same channel cannot be retried for X before its cooldown elapses (defaults: SMS 30m, Email 15m, Push 5m, Voice 60m) And Then the member receives no more than N total attempts for X across all channels in a 24-hour period (configurable, default N=4)
Engagement Detection Halts Sequence
Given a member opens the magic link for message_id X or initiates a payment session for X When the engagement event is received by the orchestrator Then all scheduled and in-queue attempts for X are canceled within 2 seconds And Then no further notifications for X are sent across any channel for 30 days unless a new message_id is created And Then the engagement event is logged with engagement_type (link_open or payment_started), channel_of_engagement, timestamp, and overall outcome for X is set to "completed"
Per-Channel Attempts and Delay Configuration
Given admin settings define max_attempts and delay_between_attempts per channel When a message sequence executes for message_id X Then the orchestrator adheres to those limits for each channel And Then attempts beyond max_attempts per channel are not scheduled and are logged as "skipped_max_attempts" And Then inter-channel delay between a failed attempt and the next-channel attempt respects the configured delay for the next channel (default 2 minutes)
Attempt Outcome Logging and Audit API
Given any attempt is made for message_id X When the attempt completes with status in {sent, failed, delivered, engaged} Then an immutable log record is written containing: message_id, member_id, channel, attempt_number, template_id, link_id, status, provider_message_id, reason_code, scheduled_at, sent_at, delivered_at, engagement_at, latency_ms, initiating_event, orchestrator_decision And Then logs are retrievable via /orchestration/attempts?message_id=X within 1 second of write and ordered by attempt_number ascending And Then PII fields (phone, email) are masked in responses by default with an admin override that is audited
Quiet Hours & Time Zone Compliance
"As a resident, I want messages to arrive at reasonable local times so that I’m not disturbed during quiet hours while still receiving urgent notices when necessary."
Description

Respect member-specific time zones and configurable quiet hours for each channel. Determine time zones from profile data, geolocation, or prior interaction timestamps, and schedule sends accordingly. Provide overrides for legally time-sensitive or board-designated critical dues while still minimizing disturbance. Include holiday/weekend rules and throttling for bulk sends to reduce carrier filtering risk. Ensure auditability of why a send was delayed or escalated.

Acceptance Criteria
Time Zone Resolution and Re‑evaluation
Given a member has a profile time zone set, When any notification is queued, Then the system uses the profile time zone and logs source="profile". Given a member lacks a profile time zone but has a last geolocation with accuracy ≤ 50 km within the last 30 days, When a notification is queued, Then the system derives the time zone from geolocation and logs source="geolocation". Given neither profile time zone nor valid geolocation is present but prior interaction timestamps exist across at least 3 events, When a notification is queued, Then the system infers time zone with confidence ≥ 0.7 and logs source="inferred" and confidence value. Given no resolvable time zone, When a notification is queued, Then the system uses the community default time zone and logs source="default". Given a member’s resolved time zone changes before a scheduled send fires, When the scheduler runs, Then the send is re-evaluated and rescheduled to comply with the new local quiet hours and logs reason="timezone_changed".
Per‑Channel Quiet Hours Enforcement
Given channel-specific quiet hours are configured for a member in the member’s local time zone, When a send is attempted within a channel’s quiet hours, Then that channel’s send is deferred to the earliest minute outside quiet hours and a schedule record is created with next_attempt_at. Given a notification targets multiple channels, When the primary channel is in quiet hours but at least one alternate channel is not, Then the notification is delivered via the first non-quiet channel according to configured channel priority, and the primary channel is rescheduled. Given a deferred send due to quiet hours, When the quiet window ends, Then the system delivers within 1 minute of the window end (subject to throttling) and updates audit logs with reason="quiet_hours". Given a member has a quiet hours window that spans midnight, When scheduling across days, Then the scheduler respects the wrap-around boundary and does not send during the entire quiet interval.
Rescue Escalation Honors Quiet Hours with Critical Overrides
Given an SMS containing a magic link bounces or the link expires, When Link Rescue triggers, Then the system escalates to the next channel that is currently outside its quiet hours in the member’s local time and logs the escalation path. Given no channels are currently outside quiet hours, When Link Rescue triggers, Then the system schedules the next eligible attempt at the earliest allowed time and does not send immediately. Given a notification is flagged as critical dues with a due_at timestamp, When time until due_at ≤ 2 hours, Then the system may override quiet hours by sending Email first; if no confirmation within 30 minutes and time until due_at ≤ 90 minutes, then send SMS; if time until due_at ≤ 30 minutes and still unconfirmed, then place a single voice read-out call; all actions are logged with override_reason="critical_dues". Given a critical override is exercised, When the audit record is created, Then it includes policy version, channels used, timestamps, and the member’s quiet hours settings at time of send.
Holiday and Weekend Blackout Rules
Given an organization-level holiday calendar and weekend definition for the member’s locale, When a notification would be sent on a holiday or weekend marked as Do Not Disturb, Then the send is deferred to the next business day at the start of the channel’s allowed window. Given a notification is critical and due_at is within 24 hours on a holiday/weekend, When applying blackout rules, Then the system minimizes disturbance by preferring Email/Push over SMS/Voice and caps voice calls to at most 1 attempt within the last 30 minutes before due_at. Given a holiday rule causes deferral, When the scheduler computes next_attempt_at, Then the calculation accounts for both holiday and quiet hours boundaries and is stored in the audit log with reason="holiday_blackout".
Bulk Send Throttling and Carrier Risk Reduction
Given a broadcast to ≥ 500 recipients, When dispatching SMS, Then the system enforces configured per-carrier rate limits without exceeding them in any 60-second window and applies 0–90 seconds of randomized jitter per recipient. Given throttling is active, When queue length exceeds the configured backlog threshold, Then the system emits backpressure signals and updates expected completion time while preserving per-recipient quiet hours compliance. Given carrier errors indicating filtering or temporary blocks exceed 2% over a 5-minute window, When sending a broadcast, Then the system automatically reduces the send rate by at least 50% for the affected carrier group for 10 minutes and records the change with reason="adaptive_throttle". Given throttling defers sends beyond their originally scheduled minute, When those sends execute, Then they must not violate quiet hours, holiday rules, or escalation policies.
Auditable Send Decision Trail
Given any notification is delayed, rescheduled, escalated, or overridden, When viewing the audit trail, Then the system displays a chronological decision log including resolved time zone and source, quiet hours windows, holiday/weekend evaluation, throttle decisions, escalation steps, and override flags. Given an API request for a specific notification_id, When retrieving the audit details, Then the response includes reason codes, policy version, next_attempt_at (if pending), and all channel attempts with timestamps and outcomes, and returns within 2 seconds for the most recent 100 notifications per member. Given a compliance review, When exporting audit logs for a date range, Then the export contains all decision data and is filterable by reason code (quiet_hours, holiday_blackout, throttle, bounce, critical_dues).
Voice Read-Out Code Escalation (IVR/TTS)
"As an elderly resident without reliable texting, I want a phone call that reads me a code I can enter so that I can still pay my dues on time."
Description

For critical dues or repeated failures on other channels, place an automated call that uses text-to-speech to read a short, human-friendly code or link alias that maps to the member’s invoice. Support keypad confirmation, voicemail fallback with truncated code, language selection, and retry windows. Log call outcomes (answered, voicemail, failed) and integrate with the orchestrator to stop further attempts on success. Ensure accessibility and clear instructions for non-technical users.

Acceptance Criteria
Escalation Trigger After Channel Failures
Given a member has an outstanding invoice that is critical or has reached the configured failure threshold on non-voice channels within the lookback window And the member has a dialable phone number on file And the current time is within the permitted call window for the member’s time zone When the Link Rescue orchestrator evaluates delivery options Then it schedules and places an automated TTS call within the configured retry window And records the escalation reason and threshold values used And does not exceed the configured maximum voice attempts per day and per invoice
TTS Code Read-Out and Repeat Options
Given the member answers the call When the TTS flow starts Then a plain-language greeting states the community name and purpose of the call And a short human-friendly code or link alias uniquely mapping to the member’s current invoice is read aloud And the code is spelled using phonetics and digit-by-digit pacing suitable for comprehension And options are provided to repeat the code, slow speech rate, or receive the link via SMS/email And the code presented is valid (not expired) at the time of the call
DTMF Confirmation Stops Orchestration
Given the member is in the TTS flow and hears the options When the member presses 1 to confirm receipt of the code Then the system marks the outreach as successful and halts further delivery attempts for that invoice And the confirmation with timestamp and caller ID is logged When the member presses 2 to request a follow-up message Then the system sends the code/link via the best available non-voice channel and logs the action When the member inputs an invalid key or provides no input Then the system re-prompts up to the configured number of times and ends the call gracefully
Voicemail Detection and Safe Message
Given the call is answered by voicemail or an answering service is detected When the system switches to voicemail mode Then it leaves a safe message that excludes sensitive PII and includes a truncated code or alias sufficient for identification And provides a callback number or short code with simple next steps And the message length remains under the configured maximum duration And the outcome is logged as voicemail and the next retry is scheduled per the configured window (not immediately)
Language Selection and Locale Fallback
Given the member has a preferred language in their profile When the call is placed Then TTS voice and prompts use the preferred language if supported And the code is articulated using locale-appropriate phonetics When the member requests a different language during the call via the designated key Then the system switches languages immediately and continues the flow When the preferred language is unsupported Then the system falls back to English and logs the fallback
Quiet Hours and Time-Zone Aware Retries
Given the member has a known time zone When scheduling initial and retry voice attempts Then the system avoids calls during configured quiet hours for that time zone And spaces retries using the configured backoff rules And never exceeds the configured maximum number of voice attempts per invoice and per week When the time zone is unknown Then the system derives it from the member’s address or defaults to a safe calling window and logs the assumption
Outcome Logging and Orchestrator Integration
Given any voice attempt is made When the attempt completes Then the system records outcome details including timestamp, duration, result (answered, voicemail, busy, no-answer, failed), DTMF selections, language used, code version, and call identifiers And exposes the outcome in the admin audit trail and via API And updates the orchestrator so a success stops further attempts and a failure schedules the next best action per policy And supports exporting outcomes for a selected date range and invoice
Security, Link Integrity, and Audit Trail
"As a security-conscious board treasurer, I want links to be secure and fully auditable so that we reduce fraud risk and can verify what happened if there’s a dispute."
Description

Issue HMAC-signed, rotating-secret magic links with configurable TTL, optional single-use enforcement, and scope-limited permissions (invoice-only). Implement rate limiting, replay detection, and automatic revocation on regeneration. Store an immutable audit trail of link issuance, invalidation, channel attempts, opens, and payments with timestamps and actor/system attribution. Minimize PII in URLs and logs, and support export for compliance reviews.

Acceptance Criteria
HMAC-Signed Link Generation with Rotating Secrets
Given a valid invoice and member record When the system generates a magic link Then the token is HMAC-SHA256 signed using the current active secret and base64url encoding without padding And the payload includes invoice_id, member_id, scope='invoice', issued_at (UTC), and a cryptographically secure nonce And verification fails if any payload field is tampered or the signature does not match And verification succeeds with either the active secret or the most-recent previous secret within the configured rotation overlap window And tokens signed with secrets older than the overlap window are rejected with an invalid-signature response
TTL Expiry Enforcement and Error Handling
Given a community-level configurable TTL between 5 minutes and 7 days and a clock-skew tolerance of 120 seconds When a magic link is accessed before TTL expiry Then access is granted for the scoped resource and the access is logged to the audit trail When the same link is accessed after TTL + skew Then the system returns an Expired response (HTTP 410) without revealing resource data and records an expired_open event in the audit trail And the UI offers a safe regenerate link action (if enabled) without exposing PII
Single-Use Enforcement
Given single-use enforcement is enabled for magic links When a token is redeemed successfully for its first valid access Then the token is immediately marked as used and cannot be reused for any endpoint And a subsequent attempt with the same token returns HTTP 409 (already used) and is logged as replay_attempt in the audit trail And in the case of concurrent redemption attempts, only one succeeds and all others are rejected without duplicating payment or side effects When single-use enforcement is disabled Then the token may be used multiple times within TTL to view the invoice, while payment actions remain idempotent
Scope-Limited Permissions (Invoice-Only)
Given a magic link with scope='invoice' and an embedded invoice_id When the link is used Then the session grants access only to view/pay the referenced invoice and no other account or community resources And attempts to access any resource outside the scope (e.g., profile, other invoices, admin pages) return HTTP 403 and are logged as scope_violation And any mismatch between requested resource and the signed invoice_id results in signature verification failure and access denial
Replay Detection and Rate Limiting
Given a verification endpoint with rate limits configured When requests are made to validate or redeem the same token from the same IP/device beyond 10 attempts in 60 seconds Then the system returns HTTP 429 and enforces a cooldown of 5 minutes for that token+IP tuple And all excess attempts are logged with rate_limited=true in the audit trail When a previously used, expired, or revoked token is presented Then the system returns HTTP 410 and records a replay_attempt with token fingerprint and source IP
Automatic Revocation on Regeneration
Given the system regenerates a fresh magic link for the same member+invoice+scope (e.g., due to SMS bounce, expiry, or user-initiated resend) When the new link is created Then all prior tokens for that tuple are immediately revoked before any new delivery is attempted And any request using a revoked token returns HTTP 410 and is logged as revoked_token And the regeneration and revocation events include reason codes (bounce, expiry, manual) with timestamps and actor attribution in the audit trail And the new token uses a new nonce distinct from all prior tokens
Immutable Audit Trail, PII Minimization, and Export
Given audit logging is enabled When any of the following events occur: issuance, regeneration, revocation, channel_attempt (sms/email/push/voice), open, expired_open, replay_attempt, payment_initiated, payment_succeeded, payment_failed Then an append-only record is stored with ISO-8601 UTC timestamp (ms precision), event type, actor (system/user id), community id, invoice id, token nonce, channel (if applicable), and outcome And application-layer updates or deletions to prior audit records are disallowed; attempts to modify return HTTP 405 and are logged as audit_mutation_blocked And no URLs, logs, or audit records contain raw email addresses, full phone numbers, or names; contact values are masked or hashed; links contain only opaque IDs (no PII) And authorized roles (e.g., Board Admin, Compliance) can export audit records by community and date range to CSV and JSON; unauthorized users receive HTTP 403 And exported files include all captured fields and are checksum-verified to match the server-side query results
Admin Controls & Delivery Analytics
"As a board president, I want controls and clear analytics on delivery and payments so that I can tune our outreach and improve on-time dues."
Description

Provide a dashboard for configuring failover rules, quiet hours, and escalation criteria per community or post type. Display funnel analytics from send to open to payment, breakdown by channel, carrier error categories, and time-to-engagement. Allow manual resend/regenerate actions with guardrails, bulk remediation for detected carrier filtering, and exports for board reports. Offer alerts for anomalous failure rates and suggestions (e.g., adjust send times or verify emails).

Acceptance Criteria
Configure Failover Rules by Post Type
Given a community admin with Manage Delivery permissions When they open Link Rescue settings for a specific post type Then they can define a channel escalation order (e.g., SMS → Email → Push → Voice) with per-step delay thresholds (in minutes) and max retries And saving validates that delays meet a minimum of 2 minutes and max 5 steps And post-type rules override community-wide defaults for that post type And all changes are versioned with who/when/old/new values visible in the audit log Given a live post using these rules When an SMS bounce is detected for a recipient Then a fresh magic link is regenerated and the next channel in the sequence is attempted within the configured delay And the previous link is invalidated and marked as superseded
Quiet Hours & Time Zone Enforcement
Given member profiles with a stored time zone or inferred from location When a send is scheduled or triggered Then quiet hours are enforced per member using their local time zone; if unknown, use the community default time zone And messages falling inside quiet hours are suppressed, queued to the next permitted window, and optionally rerouted to a non-intrusive channel if configured Given an admin attempts a manual resend during a recipient’s quiet hours When the admin lacks override permission Then the action is blocked with an explanation and suggested alternate channels Given daylight saving transitions When quiet hour boundaries overlap DST changes Then sends respect the correct UTC offset and do not double-send or skip queued messages
Funnel Analytics with Channel and Carrier Breakdown
Given filters for date range, community, and post type When the Delivery Analytics dashboard loads Then it displays Sent, Delivered, Opened, Clicked, and Paid counts and rates by channel, with totals matching the sum of channels within ±0.1% And a "Last updated" timestamp reflects data freshness no older than 2 minutes from the latest event Given carrier error codes from the provider When errors are rendered Then they are grouped into categories (Spam/Filtering, Invalid Number, Opt-Out, Unavailable, Other) with a mapping reference in tooltip And clicking a category drills down to member-level events with timestamps and provider codes Given time-to-engagement metrics When viewing the KPI panel Then median, p75, and p90 times from Send→Open and Send→Payment are shown and match the time-series within ±1%
Manual Resend/Regenerate with Guardrails
Given an admin selects a failed or expired delivery for a member When clicking Resend/Regenerate Then a new magic link is generated and the prior link is invalidated And the action is rate-limited to at most 1 resend per member per 5 minutes and 5 per 24 hours And opted-out channels are blocked with guidance to use allowed channels Given a multi-select of recipients When initiating a bulk resend Then the UI enforces a maximum batch size of 1,000 per job, shows an estimated send time, and applies quiet hours/time zone rules And all actions are recorded in the audit log with initiator, reason, channels, and outcomes
Bulk Remediation for Carrier Filtering Incidents
Given the system detects an elevated filtering rate for SMS (e.g., delivery rate < 80% over 10 minutes with >500 sends) When an admin opens the Remediation panel Then a cohort of affected messages is auto-selected with the detected error category and suggested reroute channel(s) Given the admin confirms remediation When executing the batch Then messages are rerouted respecting quiet hours and provider rate limits, with a pre-flight test to a designated admin number And results show successes, failures by reason, and allow retry of partial failures Given a scheduled remediation window When the admin cancels before execution Then queued messages are removed and the cancellation is logged
Export Board Report Metrics
Given applied analytics filters When exporting Board Report as CSV or XLSX Then the export includes funnel metrics, channel breakdown, carrier error categories, and time-to-engagement statistics And excludes PII except Member ID and Community-unique alias; includes community name, date range, currency, and time zone offset in headers Given datasets exceeding 100k rows When export is requested Then a background job is created, a progress indicator is shown, and a secure download link is emailed within 15 minutes And all timestamps in the export are converted to the community time zone with offset notation And export and download events are logged with requester identity
Anomalous Failure Alerts with Actionable Suggestions
Given alert thresholds configured (e.g., SMS delivery rate < 85% over a 15-minute rolling window with ≥200 sends) When a threshold is breached Then an alert is sent via email, Slack, and in-app to admins, respecting quiet hours by using non-intrusive channels at night And the alert includes affected channels, carriers, segments, and an estimated impact Given an open alert When an admin views it Then actionable suggestions are presented (adjust send window, verify email coverage, reorder channels) with a one-click Apply that stages config changes for review before publish Given multiple identical breaches within a suppression window (e.g., 30 minutes) When evaluating notifications Then only one consolidated alert is sent with trend updates And dismissals as "Not an issue" suppress similar alerts for 24 hours and are captured in the audit log

Receipt Backtext

After payment, instantly text a confirmation with amount, timestamp, last‑4 of the method, and a reference ID. Include a link to a downloadable PDF receipt via email and an add‑to‑calendar nudge for the next due. Builds payer confidence and gives treasurers clean, exportable memos.

Requirements

Instant SMS Receipt Dispatch
"As a homeowner payer, I want an immediate text confirming my payment with key details so that I have instant proof and peace of mind."
Description

On successful payment, automatically send an SMS confirmation within seconds that includes the community name, payment amount in the payer’s locale and currency, the payment timestamp in the payer’s timezone, the last four digits of the payment method, and a unique reference ID. The message should use a configurable template and include a secure link to a hosted receipt page. The dispatch must be idempotent to avoid duplicate texts on retried webhooks, and it must respect user notification settings. Payment events from all supported gateways (e.g., card, ACH) should map to a unified payload to drive consistent messaging.

Acceptance Criteria
SMS Sent Within 10 Seconds on Successful Card Payment
Given a successful card payment event is received by the system And the payer has SMS notifications enabled with a verified phone number When the event is processed Then the SMS is queued with the SMS provider within 10 seconds of event ingestion And the SMS includes the community name, the amount formatted in the payer’s locale and currency, the payment timestamp in the payer’s timezone, the last four digits of the payment method, and a unique reference ID And the SMS content is rendered using the currently active community template And the SMS includes a secure link to the hosted receipt page
Idempotent Dispatch on Webhook Retries
Given a successful payment where the gateway retries delivery of the same success event with the same transaction or event identifier When the system processes duplicate events Then no more than one SMS is sent for that payment And duplicates are logged and ignored without triggering additional sends And the deduplication key is the transaction/reference ID combined with the notification type
Respect User Notification Preferences
Given a payer has opted out of SMS receipts or lacks a verified mobile number When a payment success event is received Then no SMS receipt is sent to that payer And the suppression reason is recorded in the audit log And no background retries are scheduled for suppressed notifications
Localized Amount and Timestamp Rendering
Given a payer with stored locale and timezone preferences When composing the SMS Then the amount is formatted per the payer’s locale with correct currency, separators, and placement And the timestamp reflects the payment time in the payer’s timezone with correct DST handling And numerals follow the payer’s locale conventions where applicable And if locale or timezone is missing, community defaults are used
Template Configuration and Safe Rendering
Given an admin-configured SMS template containing placeholders for community_name, amount, timestamp, last4, reference_id, and receipt_url When generating the SMS Then all placeholders are resolved from the unified payment payload And unknown or missing placeholders render as empty without breaking the message And control characters and HTML are stripped or safely escaped And the final message complies with provider length constraints, using segmentation when necessary
Secure Receipt Link Access
Given the SMS includes a receipt URL When a recipient opens the link Then the page loads over HTTPS And access is authorized via a signed, time-limited token tied to the payment reference and recipient And the token expires after the configured TTL (default 48 hours) and cannot be reused after expiration or revocation And the receipt page displays the same reference ID present in the SMS
Unified Gateway Event Mapping Across Payment Methods
Given a successful payment from any supported gateway (e.g., card, ACH) When the event is normalized Then the unified payload includes community_name, payer_locale, payer_timezone, amount, currency, payment_method_last4, reference_id, receipt_url, timestamp_utc, and timestamp_local And the SMS structure is consistent across gateways with only the last4 reflecting the specific method (e.g., card PAN last4, ACH account last4) And ACH pending/pre-note events do not trigger an SMS; only settled/succeeded events do
PDF Receipt Generation & Email Delivery
"As a payer, I want a downloadable PDF receipt sent to my email so that I can save or share official proof of payment."
Description

Generate a branded, tamper-evident PDF receipt for each successful payment that includes payer details, unit/account, amount, fees, timestamp, method last-4, reference ID, and memo/line items. Store the PDF securely and make it accessible via a signed, expiring URL. Send an email to the payer with a clear summary and a link to download the PDF (optionally attach the PDF if enabled). Link the same receipt from the SMS-hosted page. Track email delivery and opens, and log all artifacts for export with proper retention controls.

Acceptance Criteria
Branded, Tamper-Evident PDF Generation
Given a payment transitions to Succeeded with a unique Reference ID When the receipt service is invoked Then exactly one PDF receipt is generated within 60 seconds and linked to that payment And the PDF is branded with the community name and logo present on the first page And the PDF contains payer full name, unit/account, amount, itemized fees, community-local timestamp with timezone, payment method type and last 4, Reference ID, and memo/line items And the PDF filename matches Receipt_<ReferenceID>_<YYYYMMDD>.pdf And the PDF includes a tamper-evident signature that verifies as valid via the platform’s verifier; if any byte is modified, verification returns invalid
Secure Storage and Signed URL Access
Given a receipt PDF exists When it is stored Then it is encrypted at rest and not accessible via public or unauthenticated URLs When a signed URL is generated Then the URL includes an expiry timestamp and signature and is valid only for that receipt When the URL is requested before expiry Then the response is 200 with application/pdf and correct filename When the URL is requested after expiry or with an invalid signature Then the response is 403 and no file bytes are returned And all URL generations and accesses are logged with timestamp, IP, user agent, and Reference ID
Receipt Email Summary, Link, and Tracking
Given the payer has a deliverable email address When a payment succeeds Then an email is sent within 5 minutes with subject containing “Receipt”, the Reference ID, and the community name And the body includes amount, community-local timestamp, payment method type and last 4, Reference ID, and a prominent “Download PDF receipt” link to the signed URL And email delivery events (accepted, delivered, bounced) are captured with timestamps And an open event is recorded when the tracking pixel is loaded When the email address is missing or marked undeliverable Then the send is aborted and a corresponding event with reason is logged
Optional PDF Attachment Setting
Given the community setting “Attach PDF receipts to emails” is enabled When the receipt email is sent Then the PDF receipt is attached with MIME type application/pdf and filename Receipt_<ReferenceID>_<YYYYMMDD>.pdf When the setting is disabled Then no attachment is included And in both cases the email body includes the download link to the signed URL And if attachment creation fails, the email is sent without attachment and an 'attachment-error' event is logged
SMS-Hosted Page Receipt Link Parity
Given a successful payment triggers an SMS with a link to the SMS-hosted confirmation page When the payer opens the page and taps “View receipt” Then they are directed to the same receipt artifact referenced in the email (matching Reference ID) And access control and expiry behavior match the signed URL rules When the link has expired Then the page displays “Link expired” with guidance to obtain a new link via the portal and no PDF content is exposed
Audit Logging, Export, and Retention
Given receipt generation, storage, signed URL issuance, email send/deliver/open/bounce, SMS page clicks, and PDF downloads occur When these events happen Then an immutable audit entry is recorded for each with event type, Reference ID, actor/context, timestamp (UTC), and relevant metadata When an admin requests an export for a date range Then CSV and JSON exports are produced containing receipt attributes and associated events and are available for download Given a retention policy is configured When artifacts and logs pass the retention cutoff and are not under legal hold Then they are purged by a scheduled job and the purge is logged with counts and timestamps
Next-Due Calendar Nudge
"As a homeowner, I want a one-tap option to add the next dues date to my calendar so that I don’t miss future payments."
Description

Provide an add-to-calendar option that infers the next dues date from the community’s billing schedule or a board-configured cadence. Include a one-tap ICS link compatible with Google, Apple, and Outlook calendars, pre-filled with title, date, timezone, and a reminder. Surface the link in both the SMS receipt page and the email. If the next due date cannot be determined, prompt the treasurer to configure it and gracefully omit the link for payers until available.

Acceptance Criteria
ICS Link Shown When Next Due Date Is Inferred
Given a payer completes a payment and the system can infer the next dues date from the community billing schedule or board-configured cadence When the SMS receipt page is rendered and the receipt email is generated Then an Add to calendar link is displayed on the SMS receipt page and included in the receipt email And the link points to a downloadable .ics resource that is accessible without login And the .ics contains a single VEVENT with a populated SUMMARY, DTSTART with the community timezone, a unique UID, and a VALARM reminder And the VEVENT date equals the inferred next due date
No Link When Next Due Date Cannot Be Determined; Treasurer Prompted
Given a payer completes a payment and the system cannot infer a next dues date from any schedule or cadence When the SMS receipt page and receipt email are rendered Then no Add to calendar link is shown to the payer in either surface And the treasurer sees a prompt in the admin dashboard to configure the billing schedule/cadence with a direct link to settings And once the treasurer configures a valid schedule, subsequent receipts include the Add to calendar link
ICS File Content and Cross-Calendar Compatibility
Given the Add to calendar link is clicked When the .ics file is fetched Then the response has Content-Type text/calendar and a .ics filename And the file contains VCALENDAR (VERSION:2.0, PRODID) with a single VEVENT having UID, SUMMARY, DTSTART, DTSTAMP, and VALARM And the file imports without error into Apple Calendar (iOS/macOS), Outlook (desktop/web), and Google Calendar And on iOS and Android, tapping the link triggers the native add-to-calendar flow
Timezone and Daylight Saving Handling
Given the community timezone is configured When the .ics is generated Then the VCALENDAR includes a VTIMEZONE matching the community timezone and DTSTART uses TZID And if the next due date falls on or near a daylight saving transition, the event appears on the correct local calendar date and time across Apple, Outlook, and Google And the reminder fires relative to the local event time
Cadence Selection for Payers with Multiple Schedules
Given a community has multiple billing cadences and the paid invoice is associated with a specific schedule When inferring the next dues date Then the system uses the cadence associated with the paid invoice And if no association exists, the system uses the community default schedule And if neither is available, the date is considered undeterminable and the Add to calendar link is omitted
Link Placement and Consistency Across Surfaces
Given a payment completes successfully When the SMS receipt page is viewed Then the Add to calendar link is visible within the receipt details section and is tappable When the receipt email is received Then the Add to calendar link appears in the email body with the same destination URL as on the SMS receipt page And the link text is consistent across both surfaces
SMS Compliance & Preferences
"As a community member, I want control over receipt texts and the ability to opt out so that I only receive messages I consent to."
Description

Implement consent and compliance controls for messaging, including opt-in capture, STOP/START/HELP keyword handling, quiet hours, rate limiting, and A2P 10DLC registration where required. Mask sensitive data (only last-4) and shorten links using a branded domain. Maintain auditable consent logs per recipient and provide per-user and per-community toggles to enable or disable receipt texts. Ensure templates meet carrier guidelines for content and length, with automatic fallback to email-only when SMS cannot be delivered or is not permitted.

Acceptance Criteria
SMS Opt-In Capture & Confirmation
Given a payer without SMS consent and a valid mobile number on profile When the payer opts to receive receipt texts during checkout or in settings Then the UI presents compliant opt-in language (program name, purpose, frequency, HELP/STOP, Msg&Data rates apply) and requires explicit confirmation before enabling SMS Given explicit consent is captured When the payer completes a payment Then a single confirmation SMS is sent and a consent record is stored with phone, userId, communityId, consent wording version, source (UI/SMS), timestamp, IP, and user agent Given no consent on record When a payment is completed Then no SMS is sent and an email-only receipt is delivered; a suppression reason "no SMS consent" is logged
STOP/START/HELP Keyword Compliance
Given the system receives an inbound SMS of STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, or QUIT (case-insensitive) When matched to a known phone-community subscription Then the subscription is marked opted-out immediately, no further SMS are sent, and a single opt-out confirmation SMS is returned; event is audit-logged with timestamp and message text Given the system receives START or UNSTOP When matched to a phone previously opted-out with a stored prior consent Then SMS receipts are re-enabled and a single opt-in confirmation SMS is returned; event is audit-logged Given the system receives HELP or INFO When matched to a known phone Then a help SMS is returned including program/community name, support email/URL, and "Reply STOP to opt out"; event is audit-logged
Quiet Hours Enforcement & Email Fallback
Given a community has quiet hours enabled between 21:00 and 08:00 in the community time zone When a receipt is generated during quiet hours and the community setting "suppress SMS receipts during quiet hours" is true Then no SMS is sent; an email-only receipt is sent within 1 minute; a suppression log with reason "quiet hours" is recorded Given a receipt is generated outside quiet hours and SMS is permitted When the payer has valid consent and preferences allow SMS Then the SMS receipt is sent immediately
Per-User and Per-Community SMS Preference Toggles
Given a user-level preference "SMS receipt texts" is disabled When any receipt is generated for that user in the community Then no SMS is sent; email-only receipt is delivered; the suppression reason "user preference disabled" is logged Given a community-level preference "Enable SMS receipt texts" is disabled When any receipt is generated in that community Then no SMS is sent for any user; email-only receipt is delivered; the suppression reason "community preference disabled" is logged; the UI shows SMS toggle disabled Given a payer with SMS disabled attempts to re-enable via START When the message is received Then the user-level toggle is set to enabled if community allows SMS; confirmation SMS is returned; event is audit-logged
A2P 10DLC Registration Gating
Given a community is not A2P 10DLC registered or the campaign is unapproved/suspended When a receipt SMS would otherwise be sent Then the send is blocked; an email-only receipt is sent; an admin-visible alert indicates "A2P registration required/invalid"; event is audit-logged with carrier reason Given the community is registered and has an assigned A2P number When a receipt SMS is sent Then the message is sent from the assigned number, includes the program/community name and opt-out instruction, and observed throughput does not exceed the campaign limits
Sensitive Data Masking & Branded Link Shortening
Given a receipt SMS is composed When inserting payment details Then only the last four of the payment method are shown (e.g., "VISA •••• 1234" or "ACH •••• 6789"); no full PAN/account or personal data is included Given a PDF receipt URL is included When shortening the link Then a branded HTTPS domain owned by Duesly or the community is used; no third-party public shorteners; link contains a signed token with no PII and expires within 30 days Given the branded shortener is unavailable When composing the message Then the system falls back to a full-length branded HTTPS URL; if no branded domain is available, omit the link from SMS and include it in the email-only receipt; log reason
Template Compliance, Length Control & Delivery Fallback
Given the default SMS receipt template When rendered Then it includes community/program name, amount, timestamp, masked method last-4, reference ID, PDF link, and "Reply STOP to opt out" and conforms to carrier guidelines Given the rendered message exceeds 320 GSM-7 characters or 280 UCS-2 characters (2 segments) When sending Then non-critical content (e.g., add-to-calendar nudge) is automatically omitted or moved to the linked receipt so the final SMS is <= 2 segments Given the carrier returns a delivery failure or the number is unreachable (e.g., landline) When sending a receipt SMS Then an email-only receipt is sent within 60 seconds; the message is marked failed with carrier code; the user/community preferences remain unchanged; event is audit-logged
Treasurer Controls & Branding
"As a treasurer, I want to configure and audit receipt texts and receipts so that they reflect our community and meet our record-keeping needs."
Description

Offer an admin configuration panel for treasurers to enable/disable the feature, customize SMS and email templates with placeholders, select sender identity/number, upload logos and brand colors for PDFs, choose link versus attachment behavior, and set retention and access policies for receipts. Provide a test mode to preview messages, the ability to resend or regenerate receipts, and export of receipt memos for accounting. Record all changes and sends in an audit log.

Acceptance Criteria
Feature Toggle and Sender Controls
Given an admin treasurer is on the Controls & Branding panel When they disable the Receipt Backtext feature and save Then payment completions do not trigger SMS or email receipts And resend/regenerate actions are unavailable to non-admins And an audit log entry of type "feature_toggle" is recorded with actor, timestamp, old_value=true, new_value=false When they enable the feature, select SMS number and email sender, and save Then subsequent receipts send from the selected SMS number and email sender And if the selected sender is unavailable at send time, the system falls back to the default sender and logs a "sender_fallback" with reason
Template Customization and Placeholder Validation
Given the template editor lists supported placeholders {amount}, {timestamp}, {method_last4}, {reference_id}, {pdf_url}, {next_due_date}, {community_name} When a template contains only supported placeholders and is saved Then the preview renders with sample values and save succeeds When a template contains an unsupported placeholder token Then validation blocks save and highlights the offending token(s) When a payment completes Then outbound messages resolve placeholders from the transaction record exactly And rendered SMS content is limited to a maximum of 3 segments (<=480 GSM-7 chars or <=210 UCS-2 chars); if exceeded, send is blocked and an error is logged
PDF Branding: Logo and Colors
Given a logo file (PNG or SVG, <=1 MB) and hex color inputs for primary and secondary When the treasurer uploads the logo and saves colors Then the PDF preview shows the logo and applies colors to header, accents, and buttons And contrast for text on colored backgrounds meets WCAG AA (>=4.5:1) or the system suggests auto-adjustment before save When a new receipt is generated after branding is saved Then the PDF uses the new branding And changes are recorded in the audit log with old/new asset hashes and color values
Receipt Delivery Mode: Link vs Attachment
Given the treasurer chooses Link delivery When a receipt is sent Then the email contains a secure, expiring link to the PDF; the SMS contains a shortened secure link; no attachment is included And link access is tokenized, audited, and expires per the retention policy Given the treasurer chooses Attachment delivery When a receipt email is sent Then the PDF is attached (<=500 KB); SMS still includes a link And if the PDF exceeds the size limit, the system falls back to Link delivery and logs a warning
Retention and Access Policies
Given a retention period between 30 and 3650 days and access scope (Payer-only, Board-only, Both) When the treasurer saves the policy Then receipt links expire and stored receipts are purged according to the retention within 24 hours And access checks enforce the selected scope; unauthorized access returns 403 and is logged And policy changes are recorded with old/new values and actor in the audit log
Test Mode Previews and Safe Sends
Given Test Mode is enabled and test contacts are configured When the treasurer clicks Send Test Then SMS/email are sent only to the test contacts using sandbox sender identities And the PDF preview is watermarked "TEST — NOT A RECEIPT" and is not persisted to payer records And audit log entries are flagged type=test and excluded from operational metrics
Operational Actions: Resend, Regenerate, and Export
Given an existing payment with a prior receipt When the treasurer clicks Resend Then the latest receipt is delivered via current delivery settings to the payer’s current contacts and an audit entry records actor, reason=resend, and delivery outcome When the treasurer clicks Regenerate Then a new PDF is rendered with current branding/templates, the original reference_id is retained, a revision number is incremented, prior access links are invalidated, and the action is logged When the treasurer requests an Export for a date range and optional filters Then a CSV is generated with columns: payment_id, payer_id, amount, timestamp (ISO 8601 UTC), method_last4, reference_id, memo, delivery_mode, send_status, revision, brand_version And exports up to 100,000 rows complete within 60 seconds and are recorded in the audit log with a file checksum
Delivery Monitoring, Retries, and Idempotency
"As a treasurer, I want reliable delivery with visibility into failures so that I can ensure homeowners receive confirmations and reconcile issues quickly."
Description

Track SMS and email delivery states (queued, sent, delivered, failed) with vendor webhooks and surface them in a simple dashboard. Implement exponential backoff retries for transient failures and route to alternate channels (e.g., email-only) when SMS fails or is blocked. Use idempotency keys to prevent duplicate receipts across webhook retries and payment reversals. Provide searchable logs by payer, unit, reference ID, and date range, with export capability for treasurer audits.

Acceptance Criteria
Webhook Delivery State Tracking in Dashboard
Given a receipt message is sent, When vendor webhooks for queued, sent, delivered, or failed are received, Then the message status updates within 5 seconds of webhook receipt and the dashboard shows the latest state. Given duplicate or out-of-order webhooks for the same provider message ID, When they arrive, Then the system deduplicates and applies only forward state transitions and logs suppressed events. Given an unknown or malformed webhook payload, When it is received, Then the event is rejected without altering message state and an error log entry is created. Given a treasurer applies filters by channel, status, and date range, When the filter is applied, Then the dashboard list updates within 2 seconds for up to 5,000 records and counts match the filtered results.
Exponential Backoff Retries for Transient Failures
Given a transient delivery failure occurs, When sending a receipt, Then the system retries with exponential backoff delays of 1m, 2m, 4m, 8m (±10% jitter) up to 4 attempts and stops on success or terminal error. Given a terminal failure is returned, When the send attempt completes, Then no retry is scheduled and the status is marked failed with the provider reason code. Given retries are performed, When viewing message details, Then attempt timestamps, error codes, and next scheduled retry are visible. Given the maximum retry threshold is reached, When the final attempt fails, Then the status is failed and fallback routing is evaluated.
Alternate Channel Routing on SMS Failure or Opt-Out
Given an SMS receipt is terminally failed or the recipient has opted out, When the failure is recorded, Then an email-only receipt is sent within 2 minutes using the same reference ID and no further SMS retries occur. Given SMS is experiencing transient failures, When retries remain, Then email fallback is not triggered until retries are exhausted or a terminal error is received. Given the payer has no email on file, When SMS fails terminally, Then the system records "no alternate channel available" and surfaces this in the dashboard. Given a fallback email is sent, When viewing delivery records, Then the SMS failure and fallback email are linked by the same reference ID for traceability.
Idempotency Across Webhook Retries
Given the provider retries a webhook for the same message, When duplicate events arrive with the same provider message ID or idempotency key, Then no additional receipts are created or sent and the existing record is updated only for a new state. Given two concurrent processes attempt to send a receipt for the same payment, When they use the same idempotency key, Then only one send is executed and the other returns an idempotent replay response without creating a duplicate. Given our system retries a send request due to network uncertainty, When the vendor processes it, Then internal deduplication prevents multiple internal message records or duplicate notifications.
Idempotency on Payment Reversal Events
Given a payment is reversed after a receipt was sent, When the reversal event is processed, Then no new receipt confirmation is sent and a reversal record is linked to the original reference ID. Given the provider retries reversal webhooks, When duplicate reversal events arrive, Then reversal processing is idempotent and does not create duplicate records or notifications. Given a treasurer views the message detail, When a reversal exists, Then a Reversed badge and timestamps are displayed without sending additional messages.
Searchable Delivery Logs with Export for Audits
Given a treasurer searches by payer, unit, reference ID, status, channel, and date range, When the query is submitted, Then results return within 2 seconds for up to 5,000 matches and paginate beyond that. Given an export is requested for the current filtered results, When the export is generated, Then a CSV is produced within 60 seconds containing timestamp, payer ID, payer name, unit, channel, status, reference ID, provider message ID, attempt count, and failure reason, and the row count matches the result set. Given an export link is delivered, When accessed within 24 hours, Then the file downloads successfully; after 24 hours the link expires. Given timestamps are shown in UI or CSV, When displayed, Then UI times are in community time zone and CSV times are ISO 8601 UTC.

One‑Tap Autopay

Post‑payment, offer a single toggle to save the wallet and enroll in autopay for future dues—no account creation required. Clear terms, reminder cadence, and easy cancel links are included in the confirmation SMS. Converts one‑offs into reliable recurring payments and stabilizes cash flow.

Requirements

One-Tap Autopay Toggle at Checkout
"As a resident who just paid an invoice, I want to enable autopay with one tap using the same payment method so that I never miss future dues without creating an account."
Description

Present a post-payment toggle that allows residents to save the just-used payment method and enroll in autopay for future dues in a single action. The toggle defaults to off and displays concise, plain-language terms including what will be charged, when charges occur, how to cancel, and any maximum charge cap. Enrollment requires no account creation and works across web and mobile. Supports card and ACH methods used in the originating checkout session, captures explicit consent on tap, and gracefully degrades if the processor or method is ineligible. The flow returns a clear success state and hooks into confirmation messaging and audit logging.

Acceptance Criteria
Toggle Visibility & Default Off Post-Payment
Given a resident completes a one-time payment with a supported method (card or ACH) When the receipt screen renders Then show a single Autopay toggle on the page And the toggle is OFF by default And display plain-language terms: what will be charged, when charges occur, how to cancel (link), and the maximum per-charge cap And if the just-used method is ineligible for autopay, render the toggle disabled with an explanatory tooltip and a Learn more link And the toggle and terms render correctly on desktop and mobile web in the latest two versions of Chrome, Safari, Edge, and Firefox
Single-Tap Enrollment Without Account
Given the Autopay toggle is visible, OFF, and the method is eligible When the resident taps the toggle to ON Then capture explicit consent (timestamp, resident identifier, IP, user agent, terms version) And save a processor token for the just-used payment method without requesting additional data And create an autopay enrollment tied to the community, unit/account, and dues category from the originating payment And do not require account creation or login at any step And return an on-screen success state within 2 seconds showing Autopay On, next expected charge date, and a cancel link
Confirmation SMS With Terms & Cancel Link
Given an autopay enrollment is successfully created When the confirmation event is emitted Then send a confirmation SMS within 60 seconds to the phone number provided at checkout (or on file) And the SMS includes: enrollment confirmation, what will be charged, when charges occur, maximum per-charge cap, reminder cadence, and a unique cancel link that requires no login And record the SMS provider message ID and delivery status callback And on delivery failure, retry up to 3 times with exponential backoff and log the final outcome
Graceful Degradation for Ineligible or Failure
Given the payment method or processor is ineligible for autopay or returns a non-2xx error When the resident attempts to toggle Autopay ON Then do not create an enrollment or save any method And keep the toggle in the OFF state And show a non-blocking error message describing the issue and offering a Try again action And emit an error telemetry event and write an audit entry for the failed attempt
Charge Timing, Reminders, and Cap Enforcement
Given an active autopay enrollment with a defined reminder cadence and maximum per-charge cap When a new dues invoice matching the enrolled category is posted Then schedule the charge for the due date at 08:00 local community time and send a reminder per the cadence before the charge And if the invoice amount is less than or equal to the cap, attempt the charge with the saved method and mark paid on success And if the amount exceeds the cap, do not charge; send a notification instructing manual payment and log a cap_exceeded event And on charge failure: for cards, retry up to 2 times at 24-hour intervals; for ACH, do not auto-retry after NSF; in all cases, notify the resident and log outcomes
Audit Log & Consent Integrity
Given an autopay enrollment is created, updated, or canceled When the event is processed Then write an immutable audit record with: resident ID, unit/account, community ID, method type (card/ACH), masked last4, processor token ID, terms version, timestamp (UTC), source (web/mobile), IP, user agent, actor, and action (enroll/update/cancel) And store a snapshot of the rendered terms and link it to the audit record And make audit records queryable by authorized admins within 2 seconds and exportable to CSV
Cancel Without Account via SMS Link
Given the resident clicks the cancel link from the confirmation SMS When the link is opened Then display a secure cancellation page (TLS, single-use token, expires after 7 days if unused and immediately after use) And allow the resident to cancel autopay with a single tap without login And update the enrollment to Canceled within 2 seconds and show an on-screen confirmation And send a confirmation SMS of cancellation and write an audit record
Tokenized Wallet Storage & Vaulting
"As a resident, I want my payment details stored securely via the processor so that autopay can run safely without me re-entering information."
Description

Securely store payment credentials via the payment processor’s tokenization/vault so autopay can run without re-entry of details. Bind the token to the resident and property/unit, and store only non-sensitive metadata (brand, last4, expiry, bank name) in Duesly. Comply with PCI DSS SAQ A by never handling raw PANs or bank account numbers, use HTTPS/TLS everywhere, and encrypt at rest all references. Handle lifecycle events such as card expiry, card updater network events, and ACH revocation; support deletion and rotation of tokens. Allow multiple saved methods and selection of a default for autopay.

Acceptance Criteria
Processor Tokenization Without Raw PAN Handling
Given a resident completes a payment and toggles One‑Tap Autopay When the payment method is saved Then the processor returns a vaulted token and Duesly stores only token ID and non‑sensitive metadata (brand, last4, expiry or bank name) And Duesly never stores or logs PAN, CVV, or full bank account/routing numbers And the token is bound to resident ID and property/unit ID And all stored references are encrypted at rest and inaccessible without service role credentials And all API calls and webhooks use HTTPS/TLS 1.2+ with HSTS enabled
Multiple Saved Methods and Default Selection
Given a resident has no saved methods When a method is saved post‑payment Then it appears in the resident wallet and is set as the default for autopay Given a resident has existing saved methods When an additional method is saved Then it is appended without overwriting existing methods And the resident can mark exactly one method as the default And autopay uses the current default method for the next run And changing the default before the next due date updates the method used
Token Deletion and Rotation
Given a resident has a saved token When they delete the payment method Then the token is disabled/deleted via processor API and removed from the resident wallet And future autopay attempts do not reference the deleted token Given a resident replaces a card or bank account When a new token is created Then Duesly rotates from old token to new token atomically And an audit event records old/new token IDs (masked), actor, and timestamp And the wallet reflects the change within 5 seconds
Card Expiry and Network Updater Handling
Given a vaulted card expires within 60 days When the nightly job evaluates saved methods Then the card is flagged as expiring and the resident is notified per preferences (SMS/email) with a manage link Given the processor card updater posts an update webhook When the webhook is validated and processed Then Duesly updates token metadata (expiry/last4 as applicable) And future autopay uses the updated token without user action And the event is recorded in the audit log Given a card updater attempt fails When the next autopay cycle runs Then the attempt retries with exponential backoff (max 3) And on final failure the resident is notified with steps to update the method
ACH Revocation and Returns Handling
Given a resident revokes ACH authorization via the manage link When revocation is confirmed Then the ACH token is suspended for debits and autopay is turned off for that resident And an audit record and resident notification are created Given an ACH return with codes R07, R10, or R29 is received via webhook When the webhook is validated and processed Then the corresponding token is suspended automatically And autopay is disabled and a notification with re‑authorization steps is sent to the resident And optional board alert is sent if enabled
PCI DSS SAQ A Scope Controls and Evidence
Given an internal audit is performed When reviewers inspect architecture and logs Then evidence shows payment data entry uses processor‑hosted fields/SDK and Duesly systems never process or store raw cardholder/bank data And TLS configuration enforces TLS 1.2+ and HSTS And token references and bindings are encrypted at rest with managed keys and access is role‑restricted And quarterly reviews of webhook access controls, key rotation, and dependency currency are documented
Autopay Enrollment Confirmation and Cancellation Links
Given a resident saves a payment method via post‑payment toggle without account creation When tokenization succeeds and the wallet entry is created Then Duesly activates autopay and sends a confirmation SMS including terms, reminder cadence, and a cancel link And following bills charge the default method on due date per terms Given the resident uses the cancel link When the request is confirmed Then autopay is immediately disabled for future charges while the token remains available unless explicitly deleted Given tokenization or wallet save fails When the post‑payment flow completes Then autopay is not activated, no token metadata is stored, and an SMS provides a manage link to retry
Autopay Scheduling & Amount Rules
"As a resident, I want autopay to run on the due date and respect a max amount so that I avoid surprises and late fees."
Description

Automatically schedule autopay for eligible dues invoices tied to the resident’s unit, with options to charge on due date, a configurable lead time (e.g., 3 days before), or the next business day when due dates fall on weekends/holidays. Respect a resident-defined maximum charge limit to prevent unexpected large withdrawals and exclude non-recurring fines or special assessments unless explicitly opted in. Handle edge cases such as invoices created after the cutoff, partial balances, late fees, and credits; ensure timezone-aware execution and idempotent processing to prevent duplicate charges.

Acceptance Criteria
Eligible Unit Dues Only
Given a resident has enrolled in One‑Tap Autopay for a specific unit and a payment method is saved, When a new invoice of type Recurring Dues is issued to that unit, Then the system schedules an autopay according to the resident’s selected schedule rule. Given an invoice is of type Fine or Special Assessment and the resident has not explicitly opted in to autopay for that category, When the invoice is issued, Then no autopay is scheduled and the invoice is flagged as Excluded:NonRecurring. Given an invoice is not in Open status (e.g., Voided or Paid), When evaluating for autopay scheduling, Then no autopay is scheduled. Given the invoice is not linked to the resident’s enrolled unit, When evaluating for autopay scheduling, Then no autopay is scheduled. Given an invoice lacks a due date, When evaluating for autopay scheduling, Then no autopay is scheduled and a reason is recorded.
Timezone‑Aware Due Date Scheduling
Given the community timezone is defined, When the resident’s schedule rule is On Due Date, Then the charge is executed on the invoice’s calendar due date in the community’s timezone regardless of server timezone. Given a daylight‑saving time transition occurs on or around the due date, When the charge executes, Then the charge timestamp falls on the correct local calendar date and is recorded with the correct timezone offset. Given residents or managers operate from different timezones, When charges are scheduled, Then the community timezone is used for all schedule calculations.
Configurable Lead‑Time Scheduling
Given a resident selects a lead time of N days before due date, When an eligible invoice with due date D is issued, Then the system schedules the charge for D − N in the community timezone. Given an eligible invoice is created after the calculated lead‑time date (D − N) but before D, When evaluating for scheduling, Then the system schedules the charge for D (subject to weekend/holiday next business day handling for due‑date scheduling).
Weekend/Holiday Next Business Day
Given an eligible invoice has a due date that falls on a weekend or on a community holiday, When the resident’s schedule rule is On Due Date, Then the charge is scheduled for the next business day in the community timezone. Given consecutive non‑business days occur (e.g., holiday followed by weekend), When calculating the next business day, Then the first subsequent non‑holiday weekday is selected. Given the resident’s schedule rule uses a lead‑time (e.g., 3 days before), When the lead‑time date falls on a weekend or holiday, Then no weekend/holiday adjustment is applied and the charge executes on the configured lead‑time date.
Max Charge Limit Enforcement
Given a resident‑configured maximum per‑charge limit L exists, When the outstanding amount A at execution time exceeds L, Then the system does not initiate a debit, marks the attempt as Skipped:MaxLimitExceeded, and leaves the invoice unpaid. Given A is less than or equal to L, When the scheduled execution runs, Then the system charges A in full. Given credits or partial payments reduce A to be less than or equal to L before execution, When the scheduled execution runs, Then the system proceeds to charge the reduced A in full.
After‑Cutoff, Partial Balances, Late Fees, Credits
Given an invoice is created after its calculated charge datetime (based on the resident’s selected schedule rule), When evaluated, Then the system schedules the charge for the next available execution window (not in the past) and records the rescheduled datetime. Given a partial manual payment is made before execution, When the scheduled job runs, Then only the remaining outstanding amount is evaluated and charged (subject to the max charge limit) and the payment record reflects the partial balance. Given credits applied before execution fully offset the invoice, When the scheduled job runs, Then no charge is initiated and the attempt is marked Skipped:ZeroBalance. Given a late fee is added to the invoice before execution, When the scheduled job runs, Then the outstanding amount includes the late fee and is evaluated against the max charge limit; the charge proceeds only if eligible.
Idempotent Processing and Duplicate Charge Prevention
Given the scheduled job is retried or multiple workers attempt the same charge for resident R, invoice I, and scheduled date S, When processing, Then exactly one successful capture is created and subsequent attempts are no‑ops logged as DuplicateSuppressed. Given a network timeout occurs after the processor received the debit request, When the job retries with the same idempotency key, Then no additional customer debit occurs and the system reconciles to a single payment record. Given overlapping schedule entries exist for the same invoice, When processing, Then only one charge attempt is executed and superseded schedule entries are canceled.
Consent Logging & Compliance Audit Trail
"As an HOA manager, I want a complete audit trail of autopay consents and changes so that we can resolve disputes and meet compliance requirements."
Description

Capture and persist explicit autopay consent details including timestamp, IP, device/user agent, consent text version, payment method fingerprint, and community/payer identifiers. Record subsequent updates and revocations with reasons where available. Generate immutable audit entries for enrollment, schedule changes, charges, retries, cancellations, and notifications. Surface proofs to admins for dispute resolution and compliance, and make records exportable for legal or processor reviews.

Acceptance Criteria
Autopay Enrollment Consent Logged After One‑Tap Toggle
Given a payer completes a one‑off payment and enables the One‑Tap Autopay toggle When the payment is confirmed by the processor Then the system persists a consent record containing: consent_id, community_id, payer_id, initiating_payment_id, payment_method_fingerprint, consent_text_version, ip_address (IPv4/IPv6), user_agent, created_at (ISO 8601 UTC) And the consent record includes processor references when available (e.g., processor_customer_id/token) And an audit entry of type "enrollment" is created linking to the consent_id with event_at (UTC) and content_hash of the consent text version And the consent record is write‑once (attempted updates are rejected and logged)
Consent Updates and Revocations Change Log
Given an existing autopay consent When the payer changes schedule details (e.g., due date, amount cap) or payment method Then a new audit entry of type "update" is appended with prior_entry_id, change_set diff, event_at (UTC), actor and channel, and optional reason And the consent record remains unchanged; effective settings are versioned and resolved from the latest audit entry When the payer cancels autopay via any channel (link, SMS reply, admin action) Then an audit entry of type "cancellation" is appended with event_at (UTC), actor, channel, and optional reason And subsequent scheduled charges are marked "cancelled" and will not execute
Charges, Failures, and Retries Auditability
Given an active autopay enrollment with a scheduled due When a charge attempt is scheduled Then an audit entry of type "scheduled_charge" is recorded with schedule_id, due_at (UTC), amount, currency When the processor run occurs Then an audit entry of type "charge_attempt" is recorded with attempt_number, event_at (UTC), amount, currency, processor_transaction_id (if created), result (success|failure), and result_code/message And on failure a "retry_scheduled" entry records next_attempt_at (UTC) and retry_policy_id And on success a "charge_settled" entry records settled_at (UTC) and receipt_id And all entries are append‑only and cannot be modified or deleted
Notification Events and Consent Proof Linkage
Given confirmation and reminder notifications are sent for autopay When an SMS, email, or push is dispatched Then an audit entry of type "notification_sent" records event_at (UTC), channel, template_id, content_hash, recipient_identifier (hashed), delivery_provider_id, and status When delivery updates arrive Then "notification_status" entries append with status (delivered|bounced|failed) and provider_message_id When a recipient clicks the cancel or manage link Then a "notification_interaction" entry records event_at (UTC), link_type (cancel|manage), and resolves to the corresponding cancellation or update audit entry
Admin Dispute View: Surfacing Proof of Consent
Given an authenticated community admin with audit permissions When the admin searches by payer, community, and date range Then the system returns the consent record and a chronological audit timeline (enrollment, updates, charges, retries, cancellations, notifications) within 3 seconds for datasets under 5,000 events And redacted PII is displayed (no full PAN), while fingerprints/tokens and processor refs are shown And the admin can download a time‑stamped PDF summary and the raw JSON for the same selection And an "admin_access" audit entry records the access with admin_id, filters used, and event_at (UTC)
Export for Legal and Processor Review
Given an admin requests an export for a community and date range When the export is initiated Then the system generates an immutable export (JSON and CSV) containing all matching audit entries with a documented schema and UTC timestamps And the export is chunked for datasets >100k rows and delivered as a signed archive (checksum + signature) with sequence ordering preserved And export completion and download events are themselves audited with requester_id, filters, and event_at (UTC) And the export includes a verification manifest with total counts per event type and a hash of each file
Confirmation SMS with Manage Links
"As a resident, I want a confirmation text with clear details and a manage link so that I can review or change autopay easily."
Description

Send an immediate confirmation SMS upon enrollment that includes enrollment status, the next expected charge date and estimated amount, reminder cadence, and short secure links to manage, pause, or cancel autopay. Use branded, plain-language copy and include STOP/HELP compliance. Links are tokenized, single-use or short-lived, scoped to the recipient’s phone number, and redirect to a secure, lightweight management experience. Provide fallback email delivery where available and log delivery/open status to the audit trail.

Acceptance Criteria
Immediate Confirmation SMS Content and Timing
Given a payer successfully enrolls in One‑Tap Autopay immediately after a payment When the enrollment success event is recorded Then a confirmation SMS is submitted to the SMS provider within 15 seconds And the provider returns an accepted/queued status within 15 seconds And the SMS body includes: enrollment status (e.g., "Autopay On"), next expected charge date (recipient timezone), estimated amount with currency, reminder cadence description, three distinct short secure links labeled "Manage", "Pause", and "Cancel", the Duesly brand and community name, and a STOP/HELP line And the message uses plain language with Flesch Reading Ease ≥ 60 (excluding URLs and compliance text) And for reachable numbers, a delivery receipt (status=delivered) is received within 2 minutes
Secure Tokenized Manage Links
Given confirmation SMS management links are generated When inspecting any link Then the URL uses HTTPS on a Duesly‑owned short domain And contains a single‑use token with ≥128‑bit entropy and no PII in path or query And the token is scoped to the intended recipient’s E.164 phone number And the token expires after first successful use or after 24 hours, whichever comes first And any subsequent or expired use returns HTTP 401 with a "Request new link" option And link access attempts are rate‑limited to a maximum of 5 attempts per minute per IP
Lightweight Manage/Pause/Cancel Flow
Given a recipient taps a management link in the confirmation SMS When the management page loads Then it is served over HTTPS, first meaningful paint < 2 seconds on 3G, and initial payload ≤ 150 KB And it displays autopay status, next charge date/amount, and reminder cadence And actions require no account creation and can be completed in ≤ 2 taps And tapping "Pause" preselects pause with clear effect and resume option; tapping "Cancel" preselects cancel with clear effect and re‑enroll option And successful actions show on‑page confirmation and trigger a confirmation SMS; email is used if SMS is opted‑out And the experience meets WCAG 2.1 AA for contrast, focus, and keyboard navigation
STOP/HELP Compliance and Opt‑out Handling
Given the confirmation SMS is sent When a recipient replies STOP Then the number is opted out from future Duesly SMS within 60 seconds and a confirmation SMS is returned And the opt‑out event is logged with timestamp and message ID When a recipient replies HELP Then a help SMS is returned within 15 seconds containing support contact details and a help link And the original confirmation SMS includes the text "Text STOP to opt out. HELP for help."
Fallback Email Delivery
Given an enrollment confirmation SMS cannot be delivered (invalid number, carrier failure, DND, or opted‑out) And a verified payer email exists When the failure status is received or opt‑out is detected Then a confirmation email is sent within 2 minutes containing enrollment status, next charge date/estimated amount, reminder cadence, and manage/pause/cancel links And email links are independently tokenized and expire within 24 hours And the email subject/body use Duesly branding and plain language
Audit Trail for Delivery and Opens
Given a confirmation SMS is generated When message lifecycle events occur Then the audit trail records: creation timestamp; SMS provider message ID; status transitions (queued, sent, delivered, failed, opted‑out); delivery receipt timestamps; and any fallback email send with provider message ID And link click events (manage, pause, cancel) are recorded with timestamp and token ID (no PII) And management actions and outcomes are logged (paused, canceled, resumed) with actor phone number and source (SMS/email) And audit entries are immutable and queryable by community, payer, invoice, and date range
Accurate Next Charge Date, Amount, and Reminder Cadence
Given the community’s dues schedule, payer’s plan, and timezone are configured When preparing the confirmation SMS content Then the next expected charge date is computed in the payer’s local timezone and formatted as "MMM DD, YYYY" And the estimated amount includes currency symbol and cents precision, labeled "estimated" if subject to change And the reminder cadence text reflects the configured schedule (e.g., "Reminders 7 days and 1 day before charge") And edge cases (month‑end, leap year, DST transitions) produce correct dates in unit and integration tests
Passwordless Manage & Cancel Autopay
"As a resident without an account, I want to manage or cancel autopay from a secure link so that I stay in control without signing up."
Description

Offer a no-account management experience reachable from SMS/email links or by entering a phone number to receive a one-time code. Allow residents to view enrollment details, cancel or pause immediately, change the default payment method, adjust charge timing and max amount, and view recent autopay history. Reflect changes in real time across scheduling and wallet services, show clear confirmation states, and send follow-up confirmations for any critical change. Include guardrails and messaging when attempting changes close to the scheduled charge time.

Acceptance Criteria
Passwordless Access via Magic Link and OTP
- Given a resident opens a valid manage-autopay magic link from SMS/email, When the link is opened within 24 hours and not previously used, Then the resident is authenticated and routed to Manage Autopay for the correct unit. - Given a resident enters their phone number on Manage Autopay, When they request a code, Then a 6-digit OTP is sent by SMS within 10 seconds and expires in 5 minutes. - Given an OTP is entered, When it matches and is unexpired, Then access is granted; When 5 invalid attempts occur within 10 minutes, Then further attempts are rate-limited for 15 minutes and an error message is displayed. - Given a magic link is expired or revoked, When opened, Then the resident is prompted to request a new OTP and no account data is revealed.
View Autopay Enrollment Details
- Given an authenticated resident, When viewing Manage Autopay, Then the UI shows status (Active/Paused/Cancelled), next charge date and local time, configured max amount, default payment method (type + last 4), reminder cadence, and a link to terms. - Given any autopay history in the past 6 months, When viewing History, Then up to 12 most recent events display date, amount, outcome (Succeeded/Failed/Skipped/Capped), and payment method used.
Cancel Autopay Immediately
- Given an authenticated resident with Active autopay, When Cancel Autopay is confirmed, Then status becomes Cancelled immediately, all future scheduled charges are removed within 5 seconds, and a success confirmation state is shown. - Then a confirmation SMS (and email if on file) is sent within 60 seconds including community name, unit, effective date/time, and instructions to re-enroll. - Given cancellation occurs within 3 hours of the next charge, When the resident cancels, Then messaging explains whether the imminent charge will still attempt; the system guarantees no duplicate capture.
Pause and Resume Autopay
- Given Active autopay, When Pause is confirmed, Then status becomes Paused immediately, the next charge is skipped, and the next eligible charge date is displayed. - Given Paused autopay, When Resume is confirmed, Then the schedule is recalculated per current timing and displayed, and a confirmation SMS is sent within 60 seconds.
Change Default Payment Method
- Given an authenticated resident, When adding a new card or bank account, Then inputs are validated, tokens are created via the wallet service, and raw sensitive data is not stored by Duesly. - When a payment method is set as default, Then future autopay uses it, scheduling and wallet services reflect the change within 5 seconds, and a confirmation SMS is sent; When tokenization fails, Then the resident sees a clear error and no default change occurs.
Adjust Charge Timing and Max Amount
- Given Active or Paused autopay, When the resident updates charge day (1–28), time (on the hour), and/or max amount (>= $0), Then the inputs are validated and a preview of the next charge date/time in the resident’s local time zone is shown before saving. - When the changes are saved, Then the schedule is updated and the next charge date/time reflects the new settings; When within 3 hours of the next charge, Then changes apply from the following cycle and this is clearly indicated.
Real-Time Propagation and Audit Logging
- Given any critical change (cancel, pause, resume, payment method change, timing, max amount), When saved, Then updates propagate to scheduling and wallet services within 5 seconds with up to 3 retry attempts and idempotent requests. - Then an audit log entry is recorded including timestamp, actor phone, action, previous vs new values, and source (OTP/Magic Link) and is available to admins; When propagation ultimately fails, Then the resident is shown a non-dismissable error and prompted to retry, leaving the previous state intact.
Failure Handling, Retries, and Notifications
"As a resident, I want failed autopay attempts to retry and alert me with a quick fix link so that my payment still goes through without penalties."
Description

Implement smart dunning for failed autopay attempts with reason-aware retry schedules (e.g., insufficient funds vs. network error), configurable retry limits, and grace-period logic to minimize late fees when retries succeed. Notify residents promptly with the failure reason and a one-tap link to update the payment method or pay now, and optionally fall back to a saved backup method if permitted. Consume processor webhooks for real-time status, ensure idempotent charge orchestration, and log all events to the audit trail and manager reporting.

Acceptance Criteria
Reason-Aware Retry Scheduling
Given a failed autopay attempt with a processor-provided failure reason code and a configured per-reason retry policy (intervals and limits) When the failure is ingested and classified Then the system schedules retries using the configured intervals for that reason and caps attempts at the configured limit And the system does not schedule retries for reasons marked non-retriable in policy And the scheduled retry timestamps are persisted and visible in the audit log and manager UI And updating the retry policy affects only retries not yet scheduled unless an explicit reschedule action is taken
Grace Period and Late Fee Suppression
Given a community-level configured grace period and late fee rules for dues invoices And an autopay attempt fails When a subsequent retry or resident-initiated payment succeeds within the grace period window Then late fees are not assessed (or are automatically reversed if already applied) And the payment is marked On-Time (Recovered) in reporting And the grace-period decision (waived/applied) and rationale are recorded in the audit log And if payment succeeds after the grace period, late fees are applied per policy and logged
Resident Failure Notification With Actionable Links
Given a failed autopay attempt is recorded When the failure is classified and persisted Then send the resident an SMS within 5 minutes containing: human-readable failure reason, next scheduled retry time (if any), and one-tap links to Update Payment Method and Pay Now And the links open to the specific invoice without requiring account creation and prefill relevant details And if SMS delivery fails or the resident has opted out, send an email fallback within 10 minutes And all notification deliveries, content templates used, and link click events are logged to the audit trail
Backup Payment Method Fallback With Consent
Given the resident has a saved backup payment method with explicit consent for autopay fallback And the primary method fails with an eligible, retriable reason (e.g., insufficient_funds, card_declined) When the failure is processed Then the system attempts a single immediate charge on the backup method using a distinct idempotency key tied to the same invoice And if the backup succeeds, the invoice is marked Paid, remaining primary retries are canceled, and the resident is notified of the backup usage And if the backup fails, the system proceeds with the configured retry schedule for the primary method (unless policy disables it) and logs both attempts And fallback is never attempted for non-retriable or authentication-required reasons unless user updates the method
Idempotent Charge Orchestration and Duplicate Prevention
Given duplicate webhooks and/or retried background jobs may occur When processing a payment attempt or scheduling/performing a retry Then the system uses deterministic idempotency keys per invoice per payment method to ensure at most one successful capture And duplicate or out-of-order events do not create additional charges and are acknowledged safely And concurrent workers cannot double-charge due to transactional locking or compare-and-swap on attempt state And the audit trail shows a single successful capture with deduplicated related events
Real-Time Webhook Processing and State Mapping
Given payment processor webhooks for authorization, capture, and failure events are enabled When a webhook is received Then the system verifies the signature and timestamp, persists the raw payload with a correlation ID, and acknowledges within 2 seconds And maps processor statuses and failure codes to an internal reason taxonomy And updates the internal payment attempt state and schedules retries/notifications accordingly within 60 seconds of receipt And gracefully handles out-of-order deliveries by reconciling the latest terminal state and ignoring already-processed events
Comprehensive Audit Trail and Manager Reporting
Given any autopay failure, retry scheduling, retry attempt, fallback attempt, notification, or eventual success occurs When the event is processed Then an append-only audit record is created with: invoice_id, resident_id, attempt_id, method type/last4, raw and mapped reason codes, timestamps, amount, actor (system/user), decision outcomes (retry scheduled, grace fee waived/applied), and notification delivery metadata And managers can view and filter reports by date range, property, status, and failure reason, and export CSV And reporting distinguishes recovered payments (via retry/fallback) from initial on-time payments And sensitive data (PAN, full tokens) is never exposed; only masked values appear in UI/exports

Guided Capture

Step‑by‑step camera prompts ensure every required photo is taken correctly—front, side, street view, and setbacks. Auto‑labels, timestamps, and location metadata attach to each shot, producing review‑ready images the first time and cutting repeat submissions.

Requirements

Capture Templates
"As a community manager, I want predefined photo templates for each inspection type so that volunteers always capture the right set of images the first time."
Description

Provide admin-configurable photo templates that define required shots (e.g., front, side, street view, setback), order, and guidance text per compliance type or announcement. Templates support mandatory vs. optional slots, orientation hints, and minimum photo count, and are versioned so historical records reference the exact template used. Templates are selectable when starting a capture session from a Duesly post and drive the UI flow. Enforcement ensures submissions cannot be completed until all required slots are satisfied. Localized instructions and per-community defaults ensure consistency across properties.

Acceptance Criteria
Admin creates template with ordered slots and guidance per compliance type
Given I am an admin for Community A with template management permissions And a compliance type "Exterior Inspection" exists When I create a template named "Exterior - Standard" with ordered slots: 1) Front, 2) Side, 3) Street View, 4) Setback, each with guidance text Then the template is saved with a unique ID and version = 1 And the defined slot order is preserved in preview and in the capture flow And the guidance text is stored per slot and rendered in template preview
Submission blocked until all mandatory slots are satisfied
Given a template where Front and Street View are mandatory and Side and Setback are optional And a user starts a capture session using this template When the user attempts to submit with only Front captured Then submission is blocked with a clear message listing missing mandatory slots: Street View And optional slots do not prevent submission And when Street View is captured and all other validations pass, submission succeeds
Orientation hints are presented per slot
Given a slot "Front" has an orientation hint of Landscape When the camera opens for the "Front" slot Then the UI displays a Landscape orientation hint/overlay And if a Portrait photo is taken, a non-blocking warning prompts to retake or continue And the captured photo's actual orientation is recorded in metadata
Template minimum photo count enforced for session
Given a template with minimum photo count = 4 And the user has attached 3 photos across all slots When the user attempts to submit Then the submit action is disabled with a message indicating "Add 1 more photo to submit" And when at least 4 photos are attached and all mandatory slots are filled, submit becomes enabled and succeeds
Template versions are immutable and referenced by captures
Given a completed historical capture used template "Exterior - Standard" version 1 When an admin edits the template and publishes changes Then a new version 2 is created and version 1 remains immutable And new capture sessions use version 2 by default And viewing the historical capture shows the template reference as "Exterior - Standard v1" with its original slots and guidance
Template selection and per-community defaults drive capture flow and metadata
Given Community A has a default template "Exterior - Standard v2" for compliance type "Exterior Inspection" And a Duesly post of that compliance type exists When a user starts a guided capture from the post Then the default template is preselected and the user may switch to any allowed template for that type And the capture steps follow the selected template's slot order and labels And each captured photo is auto-labeled with its slot name and includes timestamp and GPS location metadata attached to the submission
Localized guidance with fallback behavior
Given a user with locale "es-ES" starts a capture using a template that has Spanish translations for all slots except "Setback" When the user progresses through the capture steps Then translated slot guidance displays in Spanish And for "Setback", guidance falls back to the template's default language And the submission record stores the user's locale "es-ES"
Guided Capture Prompts
"As a volunteer inspector, I want on-screen guidance for each photo so that I can capture compliant images without guesswork."
Description

Implement a step-by-step capture experience with visual overlays, framing guides, and clear instructions for each required shot. Show progress indicators, per-shot retake controls, and allow skip with reason when enabled by policy. Use native camera APIs for low-latency capture, support both portrait and landscape, and provide accessibility-friendly text-to-speech prompts. The flow should be resilient to interruptions (calls, app switches) and resume at the correct step. Works on iOS/Android and responsive mobile web with graceful feature fallbacks.

Acceptance Criteria
Step-by-Step Shot Guidance with Overlays and TTS
Given a capture session requiring shots: Front, Side, Street View, and Setbacks When the user enters each step Then the camera view displays the correct overlay and framing guides for that shot type And the on-screen instruction text for the shot type is visible And a TTS voice prompt plays the instruction within 1 second if device TTS is available and enabled And the Next/Continue control is disabled until a photo is captured or skipping is permitted by policy And the captured photo is auto-labeled with the shot type
Progress Indicator Accuracy
Given there are N required shots configured When the session starts Then the progress indicator shows 0 of N complete When a shot is captured and accepted Then the progress indicator increments by 1 and reflects the current step (e.g., "2 of N") When a shot is retaken prior to acceptance Then the progress indicator does not increment When a shot is skipped Then the progress indicator does not increment and the step is marked as Skipped
Per-Shot Retake Controls
Given a photo has been captured for the current step Then a Retake control is visible and reachable with standard touch targets (>=44x44pt) When Retake is tapped Then the prior image is discarded and the camera returns to the same step with the same overlay When the user proceeds without retake Then the accepted image is persisted for upload and the flow advances to the next step
Skip with Reason (Policy-Gated)
Given skip-with-reason policy is enabled When the user taps Skip on a step Then a modal requires the user to select a reason from a list or enter free text of at least 5 characters And the skip event is saved with timestamp, user id, step id, and reason And the flow advances to the next step Given skip-with-reason policy is disabled Then the Skip control is not shown on any step
Native Capture Performance and Metadata (with Web Fallbacks)
Given the native iOS or Android app is used When the shutter is pressed Then shutter-to-thumbnail time is 400 ms or less on reference mid-tier devices And the photo saves at full device resolution in the background without blocking navigation And EXIF metadata includes UTC timestamp and GPS coordinates when permission is granted (horizontal accuracy <= 50 m) or records "Location unavailable" when not granted or accuracy not achieved within 3 seconds And the file name includes the shot type label and ISO 8601 timestamp Given the responsive mobile web client is used When getUserMedia is supported Then the app uses it to capture photos and displays overlays as semi-transparent masks When getUserMedia is not supported Then the app falls back to accepting gallery uploads and shows static framing tips And TTS uses the Web Speech API if available; otherwise only text instructions are provided
Orientation Support and Overlay Rotation
Given the device orientation is portrait or landscape When the orientation changes during a capture step Then overlays, guides, and UI controls rotate to match within 300 ms And safe-area insets are respected so controls are not obstructed by notches or system bars And captured images are saved with correct orientation metadata and without unintended letterboxing or stretching And users can switch orientation mid-step without restarting the session
Resume After Interruption
Given the user is interrupted by a phone call, app switch, or backgrounding during capture When the user returns within 10 minutes Then the flow resumes at the last unfinished step with previously captured photos intact and without duplicates When camera or location permissions change during the interruption Then the user is prompted to re-enable or proceed with reduced functionality and the resume continues after resolution When the app is terminated or the session is older than 10 minutes Then on reopen the session restores to the last completed step and indicates remaining steps
Auto Metadata & Labels
"As a compliance reviewer, I want each photo to include accurate labels and metadata so that I can verify when, where, and by whom it was taken."
Description

Automatically attach standardized labels (slot name), timestamps (UTC with local offset), GPS coordinates with accuracy, user and device identifiers to each captured photo. Embed metadata in EXIF and store server-side, generating a content hash and signed record to prevent tampering. Apply consistent file naming conventions for export and retrieval, and restrict post-capture edits to labels and notes per policy. Metadata is displayed in the review UI and carried through to exports and audit logs.

Acceptance Criteria
Auto-Labeling by Capture Slot
Given a user captures a photo in a required slot (Front, Side, Street View, Setback) When the photo is saved Then the photo label equals the slot name and is attached as metadata and displayed in the review UI Given a photo is captured via Guided Capture When the photo is exported Then the label is preserved in embedded metadata and in export manifests and audit logs Given policy allows label edits When a user edits the label in the review UI Then only label and notes fields are editable, all other metadata fields are read-only, and the edit is versioned and audit-logged Given policy forbids label edits When a user attempts to change the label Then the action is blocked and the attempt is audit-logged
Timestamp with UTC and Local Offset
Given device time is available When a photo is captured Then the capture timestamp is recorded in UTC (ISO 8601) and the local offset (±HH:MM) at time of capture is stored alongside it in metadata Given a photo has been captured When metadata is embedded Then EXIF/XMP contains the UTC timestamp and the local offset, and the server record stores the same values without transformation Given the review UI or an export is generated When the photo metadata is displayed Then both UTC timestamp and local offset are shown and match the stored server values
GPS Coordinates and Accuracy Capture
Given the device provides location services When a photo is captured Then latitude and longitude are captured in WGS84 with at least 6 decimal places, and horizontal accuracy (meters) is recorded in metadata and server-side Given GPS is unavailable or denied When a photo is captured Then metadata records locationStatus="unavailable" and no synthetic coordinates are written Given a photo with location metadata When exported or viewed in the review UI Then coordinates and accuracy are present and match the stored server values
User and Device Identification Metadata
Given an authenticated user captures a photo via Guided Capture When the photo is saved Then metadata includes userId, userDisplayName, deviceModel, osVersion, appVersion, and captureMethod="GuidedCapture" Given a photo has been saved When metadata is displayed in the review UI or included in exports Then the user and device identifiers are present, read-only, and match audit logs
EXIF Embedding, Content Hash, and Signed Server Record
Given a photo is captured When metadata is written Then EXIF/XMP embeds label, UTC timestamp, local offset, GPS, and device fields, and the server computes a SHA-256 hash of the binary and stores the base64 digest Given a photo record is persisted When the server finalizes the record Then the server signs the record to produce a verifiable signature and stores verification status="verified" Given a photo file or metadata is altered outside the system When verification is run on retrieval Then hash/signature verification fails, the item is marked as tampered, and the modification is blocked and audit-logged
Consistent File Naming, Export Carryover, and Audit Logging
Given a photo is saved When a filename is assigned Then it matches the pattern ^[A-Za-z0-9-]+_[A-Za-z0-9-]+_\d{8}T\d{6}Z_[A-Za-z]+_[a-f0-9]{8}\.jpg$ and contains communityId, unitId, UTC timestamp, slot label, and first 8 chars of the content hash Given a photo is exported (ZIP/CSV/PDF) When the export is generated Then the assigned filename is preserved and a CSV/JSON manifest includes label, UTC timestamp, local offset, lat, lon, accuracyMeters, userId, deviceModel, osVersion, appVersion, and contentHash Given actions occur on a photo When viewing audit logs Then create/edit/export events include photoId, UTC timestamp, userId, action, previous/new values for editable fields, and checksum verification status
Post-Capture Edit Restrictions and Auditability
Given a captured photo in the review UI When a user attempts to edit metadata Then only label and notes are editable per policy; timestamp, GPS, user/device, content hash, and signature fields are non-editable Given an edit to an allowed field occurs When the change is saved Then a new audit log entry is created with editor userId, UTC timestamp, previous value, new value, and reason (if provided) Given exports are generated When metadata is included Then the latest label and notes are included, while immutable metadata remains unchanged from the original capture
Location Validation Geofence
"As a board member, I want location validation during captures so that submitted photos are verified to be from the correct property area."
Description

Validate that captures occur within the target property or community boundary using geofences and address proximity checks. Display map preview and GPS accuracy, warn or block submissions taken outside allowed zones, and support supervised overrides with required justification. Respect user location permissions, avoid background tracking, and cache boundaries for low-latency checks. Handle spotty GPS by allowing a grace radius and retrying for improved accuracy before prompting the user.

Acceptance Criteria
In-Bounds Capture Allowed
Given the user has started Guided Capture for a specific property When the current GPS fix has horizontal accuracy <= 25 m and the coordinate is inside the target geofence Then the capture flow allows photo capture without warnings And Then the UI shows a "Within boundary" state and green map indicator And Then each captured image is tagged with latitude, longitude, accuracy (m), timestamp (ISO 8601), and geofence ID And Then the geofence membership check completes in <= 200 ms p95 after each location update
Out-of-Bounds Submission Blocked With Warning
Given Guided Capture is active When the device location is > 30 m outside the geofence and accuracy <= 50 m Then the Submit action is disabled and a blocking warning explains the out-of-bounds status And Then the user is offered actions: "Retry location" and "Request supervised override" And Then no images can be uploaded until the user is in-bounds (per check) or an override is approved
Supervised Override With Mandatory Justification
Given the user is out-of-bounds or accuracy > 50 m When the user requests an override and a supervisor authenticates and enters a justification of at least 10 characters Then the system allows submission and flags the capture as "Override" And Then an audit record is stored with capture ID, requester ID, supervisor ID, reason text, coordinates, accuracy, geofence ID, and timestamps And Then overrides are not available when the user is already in-bounds with accuracy <= 25 m
Map Preview and GPS Accuracy Display
Given location permission is granted When Guided Capture is launched Then a map preview renders the geofence polygon(s), current position dot, and accuracy circle within 1.5 s median And Then the UI displays Accuracy in meters and a quality label: Good (<= 15 m), Fair (16–50 m), Poor (> 50 m) And Then the map and accuracy readout update at least every 2 seconds while the capture step is active
Spotty GPS: Grace Radius and Retry Logic
Given a geofence check fails or GPS accuracy > 25 m When attempting location validation Then the app performs up to 3 retries over 15 seconds with exponential backoff before prompting the user And Then during low accuracy, membership is also checked against the geofence expanded by a 30 m grace radius; if inside and accuracy <= 50 m, allow proceed with a soft warning And Then if after retries accuracy remains > 50 m or the point is outside geofence+grace, show guidance to improve signal and keep Submit disabled
Boundary Caching and Low-Latency Checks
Given a community/property is selected When Guided Capture starts with network available Then the app downloads and caches the property/community geofence polygons (<= 200 KB total) with a version ID And Then subsequent membership checks use the cache and complete in <= 100 ms p95 And Then the cache is reused offline and refreshed on the next foreground session if older than 24 hours or when a newer version ID is detected And Then if no cache is available and network is unavailable, show a notice and allow proceed only via address proximity success or supervised override
Location Permissions and Foreground-Only Access with Address Fallback
Given the user has not previously granted location access When Guided Capture starts Then the app requests "While in use" location permission only and does not declare background location capability And Then if permission is denied/restricted, prompt for address confirmation and accept if the geocoded point is within 50 m of the target geofence or property centroid And Then when the app moves to background during capture, all location updates stop within 1 second and resume only on return to foreground
Image Quality Gate
"As a part-time manager, I want automatic quality checks so that I receive usable images without requesting resubmissions."
Description

Run on-device quality checks for blur, exposure, low-light, and obstruction, prompting the user to retake when thresholds are not met. Enforce minimum resolution and orientation per slot, and allow admin-configurable thresholds with optional bypass and justification. Provide real-time feedback (haptics/text) and clearly explain why a photo fails. Quality metrics are stored with the image and surfaced in review for transparency.

Acceptance Criteria
Blur Detection and Retake Prompt
Given the "Front" slot is active and the blur clarity threshold is configured When the captured image's blur score is below the configured clarity threshold Then an on-screen message "Image is blurry" appears within 300 ms And a single haptic feedback is triggered And the "Use Photo" action is disabled and "Retake" is shown And upon capturing a retake whose blur score meets or exceeds the threshold, the message disappears and "Use Photo" is enabled
Exposure and Low‑Light Quality Check
Given exposure and low-light thresholds are configured (underexposed %, overexposed %, ambient lux) When the captured image violates any configured exposure or low-light thresholds Then the failure reasons are listed explicitly (e.g., "Too dark", "Overexposed") within 300 ms And a single haptic feedback is triggered And the "Use Photo" action is disabled and "Retake" is shown And upon retake that satisfies all configured thresholds, the failure reasons are cleared and "Use Photo" is enabled
Obstruction Detection
Given obstruction detection is enabled with a configured occlusion threshold When the captured image's estimated obstructed area meets or exceeds the threshold Then the app overlays a highlight of the obstructed region and shows "Obstruction detected" within 300 ms And the "Use Photo" action is disabled and "Retake" is shown And upon retake below the obstruction threshold, the overlay and message are cleared and "Use Photo" is enabled
Per‑Slot Resolution and Orientation Enforcement
Given the "Side" slot requires minimum resolution and landscape orientation When the user captures an image below the minimum resolution or in portrait orientation Then the app shows a specific message indicating the unmet requirement(s) within 300 ms And the "Use Photo" action is disabled and "Retake" is shown And on capturing an image that meets both resolution and orientation requirements, the message is cleared and "Use Photo" is enabled
Admin‑Configurable Thresholds Persist and Apply
Given an admin updates blur, exposure, low-light, obstruction, resolution, and orientation thresholds and saves When a user begins a new capture session after the update Then the device enforces the updated thresholds for all subsequent captures And the changes persist across app relaunch and offline use And if the device has not synced, the prior thresholds remain until sync completes, after which the new thresholds apply within 60 seconds
Bypass with Justification and Audit Trail
Given a photo fails one or more quality checks and the user has bypass permission When the user selects "Bypass" Then the app requires a justification of at least 15 characters before enabling "Use Photo" And the image is accepted with a "Bypassed" flag and includes failure reasons and all quality metrics And an audit record is stored with user ID, timestamp, slot, metrics snapshot, and justification And users without bypass permission do not see the "Bypass" option
Store and Surface Quality Metrics in Review
Given a photo has been captured and saved (pass or bypass) When a reviewer opens the image in the Review screen Then the following are visible: blur score, exposure stats, ambient lux, obstruction %, resolution, orientation, pass/fail per check, timestamp, location, and bypass flag/justification if applicable And the metric values match the metadata stored with the image And reviewers can toggle a "Quality metrics" panel without affecting the image And if any metric is missing, the UI shows "Metric unavailable" for that field and logs an error event
Offline Capture & Sync
"As a field volunteer, I want to complete guided captures offline so that I’m not blocked by poor reception on-site."
Description

Allow full guided capture without connectivity by queueing images and metadata locally with encryption at rest. Provide clear offline indicators, storage usage warnings, and a background sync process with retry/backoff once online. Maintain capture order, deduplicate on re-upload, and resolve conflicts by preserving the original timestamps and template version. Administrators can set media retention limits for unsynced items to manage device storage.

Acceptance Criteria
Offline Guided Capture with Local Queueing
Given the device has no internet connectivity, When the user starts a Guided Capture session, Then the app allows completing all required steps (front, side, street view, setbacks) without errors. Given a photo is captured offline, When it is saved, Then it is added to a local queue with auto-labels, GPS coordinates, ISO 8601 UTC timestamp, capture order index, and template version. Given a session is completed offline, When the user finishes, Then the session is marked Pending Sync with a unique session ID and the queue count increases accordingly.
Encryption at Rest for Unsynced Media
Given an unsynced photo exists on the device, When its on-disk file is inspected, Then its contents are encrypted at rest and not readable as a standard image (no plaintext EXIF or JPEG header). Given the user logs out or clears app data, When the app storage is checked, Then all unsynced media and associated keys are wiped and cannot be opened by the app. Given device cloud backup runs, When backups are reviewed, Then unsynced media is excluded from cloud backups per policy.
Offline Indicators and Storage Warnings
Given connectivity is unavailable, When the user is in the capture flow, Then a persistent Offline indicator is visible within the header within 1 second of state change. Given connectivity is restored, When the app detects network, Then the indicator switches to Online within 2 seconds. Given unsynced media usage exceeds 80% of the configured retention limit, When the user is capturing, Then a storage warning banner appears showing estimated remaining captures and a link to manage storage.
Background Sync with Retry and Backoff
Given the device comes online and unsynced items exist, When the app is foregrounded or backgrounded (allowed by OS), Then background sync starts within 10 seconds and displays progress (items remaining) when in foreground. Given an upload attempt fails due to transient network or 5xx responses, When retrying, Then the client uses exponential backoff with jitter starting at 5 seconds and capping at 5 minutes, with a maximum of 10 retries per batch. Given the app is terminated during sync, When reopened online, Then sync resumes from the last successfully uploaded item without duplicating uploads.
Order Preservation and Deduplication on Sync
Given a session captured steps in order 1..N offline, When the session is synced, Then the server receives items with the same order indices and displays them in the original order. Given an item with the same session ID and content hash has already been uploaded, When a retry occurs, Then the server accepts the request idempotently and no duplicate record is created. Given partial uploads previously succeeded, When the client retries the same items, Then deduplication prevents additional charges, notifications, or records from being created.
Conflict Resolution Preserving Timestamps and Template Version
Given a session was captured offline using template version V and the server now has template version V+1, When syncing, Then the uploaded session is stored against version V and the original capture timestamps are preserved (ISO 8601 UTC). Given the server holds a record with the same session ID from a prior partial sync, When conflicting metadata is detected, Then conflict resolution keeps the original capture timestamps and template version while merging non-conflicting fields, and logs the conflict with a reference ID. Given a clock skew exists on the device, When timestamps are uploaded, Then the server stores both client-capture time and server-receipt time without overwriting the client-capture timestamp.
Admin-Defined Retention Limits for Unsynced Media
Given an administrator sets a retention limit (size in MB/GB and/or age in days) for unsynced media, When the app receives the configuration, Then the setting is applied within 5 minutes and displayed in the storage management view. Given the retention limit is reached, When the user attempts new captures, Then the app shows a blocking modal with options to review/delete unsynced items and prevents silent data loss (no auto-deletion without explicit confirmation). Given the user deletes items from the unsynced queue, When storage is recalculated, Then the warning state clears automatically once usage falls below 80% of the configured limit.
Review-Ready Export
"As a compliance reviewer, I want a one-click export of the captured set so that I can share a complete, verifiable package for decisions or appeals."
Description

Generate an ordered, labeled gallery and printable PDF package that includes each required shot with labels, timestamps, coordinates, and a map snippet. Provide a secure shareable link with role-based access and expiry, and attach the package to the originating Duesly post (announcement, payment, or compliance case). Ensure exports are immutable snapshots with an audit trail and content hash for future verification.

Acceptance Criteria
Export Includes Required Shots and Metadata
Given a Guided Capture session linked to a Duesly post has all required shots complete (front, side, street view, setbacks) with captured timestamps and GPS When the user generates a Review-Ready Export (gallery view) Then the gallery includes each required shot exactly once in the template-defined order And each image is labeled with its shot type And each image shows its capture timestamp in the community-configured timezone and stores UTC in metadata And each image shows latitude and longitude to 5 decimal places and embeds EXIF GPS And a map snippet centered on the capture coordinates is rendered per image at zoom level 17 And no placeholder or missing shots appear
Printable PDF Package Generation
Given a Review-Ready Export exists for a completed Guided Capture session When the user selects Download PDF Then a single PDF package is generated within 10 seconds for exports under 25 images And the PDF includes a cover page with post title, export ID, created-by, and created-at (UTC) And an index page lists each shot label with corresponding page number and capture timestamp And each shot page contains the full image, label, timestamp, coordinates, and the map snippet for that image And the PDF page footer displays export ID and page X of Y
Secure Shareable Link with Role-Based Access and Expiry
Given a Review-Ready Export exists When an authorized user creates a shareable link and selects allowed roles (Board, Manager, Homeowner, External Reviewer) and an expiry date/time Then the system generates a URL containing an unguessable token with at least 128 bits of entropy And only authenticated viewers with an allowed role can access the export; all others receive 403 And requests after the expiry timestamp receive 410 Gone And the link owner can revoke the link, after which access returns 410 And the shared page sets noindex and disables directory listing And all access attempts (success and failure) are logged to the export audit trail
Attachment to Originating Duesly Post
Given an export is generated from an announcement, payment, or compliance case When generation completes Then the export is attached to the originating post’s timeline as type "Review-Ready Export" And the attachment is visible to users with permission to view the post and the export And opening the attachment navigates to the export gallery with options to copy share link and download PDF And the attachment persists if the post is archived and is read-only
Immutable Snapshot and Versioning
Given a Review-Ready Export has been created When original photos or capture metadata are edited or deleted later Then the existing export’s images, labels, timestamps, coordinates, map snippets, and PDF remain unchanged And any attempt to edit the export content or metadata is blocked and logged And creating a new export produces a new export ID and does not overwrite prior exports And prior share links remain associated with their original export state
Comprehensive Audit Trail
Given a Review-Ready Export exists When any of the following occurs: export created, PDF downloaded, gallery viewed, share link created, share link revoked, access attempt (success/failure) Then an audit entry is recorded with export ID, action, actor (or anonymous), timestamp (UTC), IP, and user agent And audit entries are immutable, ordered chronologically, and retrievable by Board and Manager roles And the audit trail is included as a downloadable JSON in the export details
Integrity Hash Generation and Verification
Given a Review-Ready Export is created Then the system computes a SHA-256 content hash for the PDF and for the gallery bundle (ZIP) and stores them with the export And the export details page displays each hash value When a user runs Verify Integrity Then the system recomputes hashes and reports Pass if they match stored values, else Fail And any mismatch blocks new sharing, raises an alert to Board and Manager roles, and is logged in the audit trail

Plot Overlay

Trace the project on a satellite or parcel map, snap to lot lines, and drop dimensions and setback distances. The overlay travels with the request so reviewers instantly see placement and potential encroachments—speeding decisions and reducing disputes.

Requirements

Parcel & Satellite Base Layers Integration
"As a homeowner submitting a project, I want to view satellite imagery and parcel lot lines together so that I can accurately trace my project in the correct location."
Description

Integrate high-resolution satellite imagery and parcel/lot-line GIS layers as selectable base maps within the Plot Overlay canvas. Support community-boundary clipping, address/geocode centering, and configurable zoom levels. Cache tiles for smooth pan/zoom, handle provider outages with graceful fallbacks, and normalize coordinate systems to a single projection used across Duesly. Expose a simple layer toggle and opacity control so users can trace against the most useful context while ensuring consistent rendering in web and mobile clients.

Acceptance Criteria
Base Layer Toggle & Opacity Control
Given the user opens the Plot Overlay canvas on web or mobile, When the layer toggle is invoked, Then the options include Satellite and Parcel Lines. Given a base layer is selected, When the user switches layers, Then the new layer renders within 500 ms without flicker and with correct provider attribution visible. Given the opacity control is visible, When the user adjusts opacity from 0% to 100% in 5% increments, Then the map updates in real time and the control is keyboard-accessible and screen-reader labeled. Given a user changes layer or opacity, When the same request is reopened, Then the last-used layer and opacity are restored for that request.
Community Boundary Clipping & Mask
Given a community boundary polygon exists, When the map loads, Then the view fits the boundary with 5% viewport padding. Given boundary clipping is enabled, When the user pans or zooms, Then the map center cannot move outside the boundary and areas outside are visually masked or desaturated. Given no boundary is configured, When the map loads, Then the default world view is shown and no mask is applied.
Address and Parcel ID Geocode Centering
Given a valid address within the community, When the user searches, Then the map centers on the result within 800 ms and the zoom level shows approximately a 50 m radius. Given a parcel ID indexed in GIS, When searched, Then the centroid of the parcel is centered and the parcel boundary is highlighted for 3 seconds. Given the result is outside the community boundary, When returned, Then the user is notified Outside community and the view fits the community boundary instead. Given the geocoder is unreachable, When a search is attempted, Then a non-blocking error is shown and no map state changes.
Configurable Zoom Levels & Fit
Given minZoom and maxZoom are configured per community, When the map initializes, Then interactions honor those limits and Fit to Community frames the boundary with 5% padding. Given the current zoom equals maxZoom or minZoom, When the user tries to exceed it, Then zoom controls are disabled and gesture zooming is clamped. Given the provider max native zoom is lower than maxZoom, When the user attempts to zoom in further, Then the app clamps to the provider max and prevents overscaling.
Tile Caching & Smooth Pan/Zoom
Given tiles have been fetched in the current session, When the user pans back over the same area, Then cached tiles are reused and no network requests are made for those tiles. Given the user pans or zooms into new areas, When tiles are loading, Then placeholders are shown instead of blank space and any placeholder is visible for no longer than 500 ms. Given the user rapidly pans for 10 seconds, When observing the map, Then no tearing occurs and tiles appear progressively toward the center first.
Provider Outage Fallbacks & Resilience
Given the primary tile provider returns errors or timeouts for three consecutive requests, When rendering, Then the system switches to the configured fallback provider within 2 seconds. Given no fallback provider is configured, When the primary provider fails, Then the last successfully cached tiles are displayed with a non-blocking banner and the app remains usable. Given a provider failure occurs, When it is detected, Then the event is logged with provider name, status code, and community ID.
Coordinate System Normalization & Alignment
Given parcel GIS data is supplied in any supported CRS, When ingested, Then it is reprojected to the single projection used by Duesly (EPSG:3857) before rendering. Given normalized layers are rendered together at zoom level 19, When comparing parcel edges to satellite imagery, Then the average misalignment is less than or equal to 2 pixels across five control points. Given the same request is opened on web and mobile, When centered and zoomed to the same values, Then parcel overlays appear at identical positions with less than or equal to 1 pixel difference after scaling for device pixel ratio.
Lot-line Snap Drawing Tools
"As a homeowner, I want drawing tools that snap to lot lines so that my project outline is precise without requiring advanced mapping skills."
Description

Provide drawing tools (polygon, rectangle, line) that snap to parcel boundaries and vertices with adjustable tolerance. Enable freehand tracing with post-draw simplification, snapping hints, and vertex editing (move, add, delete) for precise overlays. Support touch and mouse interactions, keyboard modifiers for constraints (e.g., orthogonal), and an undo/redo stack. Persist draft geometry locally until saved to the request. Ensure performance for overlays up to community-lot scale without lag.

Acceptance Criteria
Snap to Parcel Boundaries and Vertices
Given the parcel map layer is available and a drawing tool (polygon, rectangle, or line) is active, when the pointer is within the current snap tolerance of a parcel edge or vertex, then the pointer and the next placed vertex snap to that target and a visual snap hint is shown. Given multiple snap candidates are within tolerance, when determining the snap target, then the closest candidate in screen space is selected, with vertices taking priority over edges in a tie. Given the user holds Alt/Option while placing or dragging a vertex, when within tolerance of a snap target, then snapping is temporarily disabled for that action. Given snapping is enabled, when drawing a rectangle or line, then each corner/endpoint placement is subject to the same snapping rules. Given no parcel features are within tolerance, when placing a vertex, then no snapping occurs and no snap hint is shown. Given the parcel layer is toggled off or not loaded, when attempting to snap, then no snapping occurs and no snap hint is shown.
Adjustable Snap Tolerance Control
Given the drawing toolbar is visible, when the user opens snap settings, then a control to set snap tolerance between 4 px and 24 px (default 12 px desktop, 16 px touch) is available. Given the user changes the snap tolerance value, when they resume drawing or editing, then snapping and visual hints use the new tolerance immediately without page reload. Given a project overlay draft exists, when the user returns later on the same device/browser, then the last chosen snap tolerance is restored from local draft settings.
Freehand Trace with Post-draw Simplification
Given freehand mode is active, when the user completes a trace, then the geometry is simplified such that the maximum deviation from the original path is less than or equal to the simplification tolerance and endpoints are preserved. Given simplification occurs, when the user is prompted, then the user can accept or undo the simplification before continuing. Given the simplification tolerance is set between 0.2 m and 2.0 m, when the user completes a new freehand trace, then the resulting geometry adheres to the selected tolerance. Given the user accepts simplification, when they invoke Undo, then the pre-simplified path is restored.
Vertex Editing (Move, Add, Delete) with Constraints
Given a drawn geometry is selected, when the user drags a vertex handle, then the vertex moves and connected edges update in real time with snapping applied. Given the user activates add-vertex on an edge, when they click/double-tap the edge or use the add control, then a new vertex is inserted at that location. Given a vertex is selected, when the user presses Delete/Backspace (or uses the delete action on touch), then the vertex is removed unless removal would violate minimum vertex count (polygon ≥3, line ≥2), in which case the action is blocked with a notice. Given the user holds Shift while drawing or editing, when creating or moving a line segment, then the segment is constrained to 0/45/90-degree increments; when drawing/resizing a rectangle, then it remains axis-aligned. Given the user holds Alt/Option while dragging a vertex, when near a snap target, then snapping is disabled for that drag.
Undo/Redo Stack for Drawing and Editing
Given the user performs an action (draw start/end, vertex move/add/delete, freehand simplify accept), when they press Ctrl/Cmd+Z or tap Undo, then the last action is reverted; when they press Ctrl/Cmd+Y or Shift+Ctrl/Cmd+Z or tap Redo, then the reverted action is reapplied. Given a new action is performed after undo, when checking the redo stack, then the redo history beyond the current point is cleared. Given a long editing session, when performing up to 50 actions, then undo/redo retains the full sequence without loss. Given an overlay up to 5,000 vertices, when invoking undo or redo, then the state change completes within 100 ms.
Local Draft Geometry Persistence Until Save
Given a user is drawing or editing, when a change occurs, then the draft geometry and tool settings auto-save locally within 1 second. Given the browser tab is closed or the page is reloaded, when the user returns within 7 days on the same device/browser, then the draft is restored automatically. Given a draft exists locally and has not been saved to the request, when a reviewer views the request, then the draft geometry is not visible to the reviewer. Given the user clicks Save to Request, when the save completes successfully, then the local draft is cleared and the saved geometry becomes the current canonical geometry for the request.
Performance at Community-Lot Scale
Given an overlay covering up to community-lot scale with up to 5,000 vertices, when panning, zooming, drawing, or editing, then the UI sustains an average frame rate ≥45 fps with no interaction latency exceeding 100 ms. Given snapping hints are rendered during movement, when moving the pointer continuously along lot lines, then hint rendering does not reduce frame rate below 45 fps. Given extended editing with 200+ undoable actions, when monitoring memory, then memory usage remains stable without unbounded growth (≤10% above baseline after returning to idle).
Setback & Dimension Annotations
"As a reviewer, I want clear dimensions and setback distances shown on the overlay so that I can quickly verify compliance without manual measurements."
Description

Allow users to drop dimension lines and labels, and configure setback distances relative to parcel boundaries and structures. Automatically compute and display shortest distances from the overlay to lot lines and any defined setbacks, with unit selection (ft/m). Provide editable style options (color, thickness, label placement) and rules to lock dimensions to geometry changes so annotations update when the overlay is edited. Validate inputs and surface warnings when dimensions conflict with configured rules.

Acceptance Criteria
Create and Edit Dimension Annotations
- Given a plot overlay is active on a parcel map, when the user selects the Dimension tool and clicks two points on the overlay, then a dimension line is created between the points with the measured length displayed as its label in the current unit. - Given a dimension annotation exists, when the user drags either endpoint or the entire overlay geometry is moved/edited, then the label updates in real time to reflect the current length. - Given a dimension label, when the user selects the label placement option (centered/inside/outside), then the label repositions accordingly without overlapping the line. - Given an existing dimension annotation, when the user deletes it, then it is removed from the overlay and from the annotations list.
Configure Setback Distances Relative to References
- Given parcel boundaries and optional structures are available, when the user opens the setbacks settings and enters a positive numeric distance for each selected reference type (e.g., parcel boundary, selected structure), then the values are saved and the setback(s) are enabled. - Given a setback value is defined, when the user applies it, then the system renders an offset line at the configured distance from the chosen reference in the current unit. - Given a defined setback, when the user edits its distance value, then all visible setback graphics and related distance calculations update immediately.
Auto-Compute and Display Shortest Distances to Lot Lines and Setbacks
- Given an overlay polygon exists, when the map is idle, then the system computes the shortest perpendicular distance from the overlay to the nearest parcel boundary and to any enabled setback line(s). - Given computed distances, when the overlay is displayed, then distance labels are shown at the nearest points and include the unit suffix (ft or m). - Given the overlay is moved, rotated, scaled, or vertices are edited, when changes are committed, then all shortest-distance labels recompute and update.
Unit Selection and Accurate Conversion
- Given the unit selector defaults to feet (ft), when the user switches to meters (m), then all inputs, labels, and computed distances convert using 1 ft = 0.3048 m, rounded consistently to two decimal places. - Given a user enters a value in the current unit, when units are toggled back and forth, then the stored value remains consistent within ±0.01 of the original after round-trip conversion. - Given unit preferences are changed, when the request is reloaded, then the last selected unit is applied.
Editable Style Options for Annotations
- Given the style options are open, when the user changes color (via hex or palette), thickness (0.5–10 px), or label placement (centered/inside/outside), then all selected dimension and setback annotations render with the new style immediately. - Given style options are changed, when the user saves the request and reloads it, then the styles persist. - Given a selected label placement would overlap geometry, when placement is applied, then the label nudges to the nearest non-overlapping position.
Annotations Locked to Geometry Changes
- Given a dimension annotation is anchored to two overlay vertices, when either vertex is moved during an edit, then the annotation's endpoints follow the vertices and the length recalculates. - Given a shortest-distance label references a parcel edge or setback line, when the overlay geometry changes, then the reference is maintained and the measured distance updates; if the reference no longer exists, a placeholder warning replaces the value. - Given the overlay geometry is deleted, when the operation completes, then all associated annotations are removed.
Input Validation and Conflict Warnings
- Given a user inputs a setback or dimension value, when the value is empty, non-numeric, zero, or negative, then the field shows an inline error and cannot be saved until corrected. - Given computed distances are less than the configured setback requirement, when the overlay is displayed, then a visible warning icon appears on the violating annotation and a message "Setback violation: required X, actual Y" is listed in the request sidebar. - Given all violations are resolved (distances >= required), when the overlay updates, then warning icons and messages clear automatically.
Automatic Encroachment Detection
"As a board member, I want the system to flag encroachments automatically so that I can focus on exceptions and make faster decisions."
Description

Evaluate overlay geometry against parcel boundaries, easements, and configured setback buffers to detect potential encroachments in real time. Surface inline warnings and highlight offending segments, with details in an issues panel. Support configurable rule sets per community (e.g., front yard setback = 20 ft) and store evaluation results with the request for auditability. Expose a summary badge in the feed to speed triage by reviewers.

Acceptance Criteria
Real-time parcel boundary encroachment detection
Given a project overlay is active on a parcel map with parcel boundary geometry loaded When the user draws, drags, or edits the overlay so that any segment crosses outside the parcel boundary Then the system computes encroachment and updates the evaluation within 500 ms of the edit completing And the offending segment(s) are highlighted and visibly distinct from compliant segments And an inline warning is displayed stating "Encroachment: Outside parcel boundary" And an issue is added to the issues panel containing: segment identifier, encroached length, and a "Zoom to" action
Setback buffer rule enforcement
Given a community ruleset is configured with front=20 ft, side=5 ft, rear=15 ft setbacks and boundary classifications are available for the parcel When the overlay placement brings any structure within less than the required setback distance for its corresponding boundary classification Then the system flags a "Setback" issue with the violated rule id, required distance, and measured minimum distance And the measured distance error is within ±0.1 ft (±3 cm) of ground truth And corresponding geometry is highlighted And the issues panel groups multiple segments under the same rule into one issue with an occurrence count
Easement encroachment detection
Given parcel easement geometry (type and extents) is available When the overlay intersects any easement Then the system creates an "Easement encroachment" issue per intersected easement with type (e.g., utility, drainage), intersection area/length, and link to easement record And intersecting geometry is highlighted And removing the intersection (by edit) clears the issue within 500 ms
Community ruleset application and unit handling
Given Request R is associated with Community A using ruleset v1.3 in feet And Community B uses ruleset v2.0 in meters with different setback values When Request R is reassigned to Community B or duplicated into Community B Then the overlay is re-evaluated using Community B's ruleset and unit system And all distances in the UI and issues panel are displayed in meters with correct unit labels And the resulting set of issues matches the expected violations from Community B's rules within ±0.1 m tolerance And the stored evaluation record includes ruleset id and version
Persisted evaluation results for audit
Given a request with an evaluated overlay is saved When the request is reopened by any reviewer Then the last evaluation results are available without re-editing, including: evaluation timestamp (UTC), ruleset id/version, geometry checksum/hash, and the full list of issues with their measurements And a label shows "Evaluated on <ISO 8601 timestamp> using ruleset <id@version>" And the same data is retrievable via API GET /requests/{id}/evaluation returning 200 and the evaluation payload
Feed summary badge for encroachment status
Given a request exists in the community feed and has been evaluated When the request has N>0 issues Then a badge is displayed on the feed item showing the count N and a severity color (red when any parcel/easement encroachment exists; amber when only setback issues exist) And when the request has zero issues, the badge displays "0" in green or is replaced by a "Compliant" indicator And clicking the badge opens the request detail view focused on the issues panel And the badge count updates within 2 seconds of saving an overlay change
Overlay Attachment to Requests & Reviewer View
"As a manager, I want the overlay to be visible within the request so that I don’t have to open external files to understand placement."
Description

Bind the saved overlay and annotations to the originating request so the geometry travels through the workflow. Embed an interactive, read-only viewer in the request detail panel, with a thumbnail in the feed. Ensure deep links open directly to the overlay view and that permissions mirror the request’s visibility settings. Provide an edit mode for submitters (until locked) and read-only for reviewers, with change indicators when updates are made after submission.

Acceptance Criteria
Overlay persists with request across workflow
Given a request has a saved overlay and annotations When the request transitions through workflow states (e.g., Draft → Submitted → In Review → Decision) Then the overlay and annotations remain bound to the request and are retrievable with the request And the latest saved overlay is displayed consistently across all request views
Read-only overlay viewer for reviewers in request detail
Given a reviewer opens the request detail panel When the overlay viewer loads Then the overlay renders interactively (pan/zoom, layer toggles, dimension tooltips) in read-only mode And no edit controls are available to non-submitters or when the request is locked And the viewer is embedded within the request detail panel and fits responsive layouts
Overlay thumbnail in the feed
Given a request with a saved overlay appears in the feed When the feed item is rendered Then a thumbnail preview of the overlay is shown on the feed card And selecting the thumbnail opens the request detail anchored to the overlay viewer And if no overlay exists, no thumbnail is displayed
Deep link opens directly to overlay view
Given a deep link URL for a request overlay is opened by an authorized user When the application routes the request Then the request detail opens with the overlay viewer in focus and the map centered on the overlay And if the user lacks permission, an access denied message is shown and the overlay is not rendered And if the link is invalid or expired, a not-found or expired link message is shown
Overlay permissions mirror request visibility
Given a user's access is governed by the request's visibility settings When the user attempts to view the overlay via detail view, feed thumbnail, or deep link Then access to the overlay is allowed or denied exactly according to the request's visibility settings And updates to the request's visibility take effect immediately for the overlay across all entry points
Submitter edit mode until lock with change indicators
Given the submitter opens their request before it is locked When they enter overlay edit mode and save changes Then geometry and annotation edits are persisted to the request's overlay And reviewers always see the overlay in read-only mode And when changes are made after initial submission and before lock, a visible "Updated since submission" indicator appears with changed elements highlighted And when the request is locked, edit controls are hidden/disabled for the submitter and the overlay is read-only for all
Versioning, Audit Log, and Autosave
"As a submitter, I want my overlay changes to autosave and be recoverable so that I don’t lose work and can revert if a mistake is made."
Description

Implement autosave for in-progress overlays and maintain version history with timestamps, author, and change summary. Allow users to add optional version notes, compare versions (diff geometry outlines and annotations), and revert when necessary. Persist all versions with immutable IDs and include version references in notifications and export artifacts to ensure a clear audit trail for disputes.

Acceptance Criteria
Autosave Draft While Editing Overlay
Given a user is editing a plot overlay on a request And there are unsaved changes (geometry, dimensions, setback markers, or annotations) When 5 seconds elapse without further input or 15 seconds pass since the last autosave, whichever comes first Then the system persists an autosave snapshot server-side with an immutable version ID, UTC timestamp, author, and auto-generated change summary And a “Saved at HH:MM” indicator appears within 1 second of completion And if the network is unavailable, the autosave is queued locally and retried with exponential backoff until success And closing the tab or a browser crash does not lose changes newer than the last successful autosave
Resume With Latest Autosaved Version
Given a request with an in-progress plot overlay that has an autosaved version newer than the last published version When an editor reopens the request Then the latest autosaved version loads as the working state and is labeled with its version ID and timestamp And the user can open Version History from the banner within one click And the audit log records the resume event with user, timestamp, and version ID
Version Metadata and Immutability
Given any save event (autosave or manual Save Version) When the system creates a version record Then it assigns a globally unique immutable version ID, stores UTC timestamp to the second, author ID, and a machine-generated change summary And version records cannot be edited or deleted via UI or API; attempts are rejected with 403 and are logged with actor and timestamp And version IDs are stable and included in internal references, exports, and notifications
Optional Version Notes on Manual Save
Given a user selects Save Version while editing a plot overlay When the user provides an optional note (or leaves it blank) Then the version is saved successfully regardless of note presence And the note (up to 500 characters, line breaks preserved) is stored with the version metadata And the note is displayed in Version History and included in related notifications and exports
Compare Two Versions (Geometry and Annotation Diff)
Given two versions of the same plot overlay are selected for comparison When the diff view opens Then geometry changes are visually highlighted on the map (added=green, removed=red, modified=yellow) And annotation differences (text, dimensions, setbacks) are listed side-by-side with changed fields highlighted And the user can toggle geometry and annotation layers on/off and see a summary count of changed items And if no differences exist, a “No differences detected” message is shown
Revert to Selected Version
Given a user with edit permissions selects an historical version of a plot overlay When the user confirms Revert Then the system creates a new head version whose content exactly matches the selected version and assigns a new immutable ID and timestamp And no existing version records are altered or deleted And the audit log records actor, fromVersionID, toVersionID, timestamp, and optional reason And subscribers receive a notification referencing the new version ID and any provided note
Include Version References in Notifications and Exports
Given a plot overlay version is shared via notification or exported (PDF/image/GIS) When the artifact is generated or the notification is delivered Then it includes the version ID, UTC timestamp, author, and version note (if any) And exported files display this metadata in the footer and embed it in file properties/metadata And artifacts include a link or identifier enabling direct navigation to the web version history
Export & Share with Scale and Legend
"As a homeowner, I want to download a clear, labeled map of my project so that I can share accurate plans with contractors or neighbors."
Description

Generate exports (PDF/PNG) that include the overlay, base layer snapshot, scale bar, north arrow, legend for annotations, and metadata (address, parcel ID, version, timestamp). Provide selectable paper sizes/orientation and resolution, and ensure exports respect permissions. Include the export file in the request’s attachments and provide a shareable link for external stakeholders when enabled by community settings.

Acceptance Criteria
Export PDF/PNG Includes Overlay, Scale, North Arrow, Legend, and Metadata
Given a request with a visible plot overlay and annotations on the map When the user exports the view as PDF or PNG Then the exported file contains the base layer snapshot, the full overlay geometry, the scale bar, a north arrow, a legend of annotation types present, and metadata including address, parcel ID, version, and timestamp And the legend lists only annotation types actually present and uses the same colors/symbols/styles and unit labels as on the map And the timestamp is ISO 8601 with timezone (e.g., 2025-08-16T14:05:00-07:00) And the overlay geometry and annotation positions match the on-screen view within ±1 pixel at 100% zoom (for PNG) or within ±0.5 mm on printed page at 100% scale (for PDF)
Selectable Paper Size, Orientation, and Resolution
Given the export dialog is open When the user selects a paper size of Letter, Legal, A4, or A3 and an orientation of Portrait or Landscape and a resolution of 150, 300, or 600 DPI Then the generated file reflects the selected size within ±1 mm, the selected orientation, and the selected resolution within ±2% And default selections are Letter, Portrait, and 300 DPI when no prior choice is made in the session And if an unsupported combination is chosen, the export action is disabled and an inline validation message identifies the invalid selection
Permissions Enforcement for Export and Sharing
Given a user without export permission on the request When they attempt to export via UI or API Then the export control is hidden in the UI and API returns 403 with no file generated Given a user with export permission on the request When they open the request Then the Export control is visible and functional Given community setting "Allow external share links" is disabled When any user attempts to generate or access an external share link Then share controls are disabled in the UI and link access returns 403, and no new links can be created And all export and share attempts (success/failure) are recorded in the audit log with user ID, request ID, timestamp, and action
Shareable Link Generation and Access Controls
Given community setting "Allow external share links" is enabled and a successful export exists as an attachment on the request When a user with permission generates a shareable link Then a unique, unguessable HTTPS URL is created with at least 128 bits of entropy and references the specific exported attachment version And accessing the link allows view/download without authentication but only the referenced file and no other request data And the link can be disabled (revoked) by an authorized user, after which access returns 410 Gone And link responses set headers to prevent indexing (X-Robots-Tag: noindex) and to use appropriate Content-Type and Content-Disposition And all link creations, accesses, and revocations are logged with timestamp and IP address
Export File Attached to Request with Metadata
Given an export completes successfully When the system saves the output Then the file is added to the request's attachments list and is immediately visible to users with attachment-view permission And the attachment stores and displays filename, file size, content type, generated timestamp, address, parcel ID, and version metadata And the attachment filename follows pattern: <Address_or_ParcelID>_plot-overlay_v<version>_<YYYYMMDD-HHMMSS>.<ext> And deleting the attachment invalidates any share link to that file within 5 seconds And if the attachment save fails, the export is not listed and no share link is produced (operation is atomic)
Scale, North Arrow, and Measurement Accuracy
Given a known-distance segment is drawn on the overlay (e.g., 100 ft or 30 m) and units are set in project settings When the export is generated at a selected paper size and DPI Then the printed or 100% on-screen measured distance derived from the scale bar matches the known distance within ±2% error And the north arrow aligns to geospatial north consistent with the base map provider for the captured view And dimension and setback labels in the export display the correct unit abbreviations (ft/m) and numeric values match the on-screen labels
Export Performance and Failure Handling
Given an overlay up to 200 vertices and up to 20 annotation elements When the user initiates an export Then the export completes within 10 seconds 95th percentile and within 20 seconds 99th percentile And if rendering exceeds 20 seconds, the job proceeds asynchronously, the user is notified, and the attachment appears when complete without requiring a page refresh And if any required map tiles or data are unavailable, the export fails gracefully with a clear error message and no partial file is attached And concurrent exports by multiple users do not block the UI and each export result is isolated to its originating request

Spec Picker

Project‑specific templates (fence, paint, roof, solar) gather the exact materials, colors, finishes, and manufacturer references your HOA requires. Quick swatch capture and link fields prevent vague descriptions, align with CC&Rs, and minimize clarification rounds.

Requirements

Template Library & Versioning
"As a community admin, I want to manage versioned templates for each project type so that submissions collect consistent, CC&R-aligned specifications across our community."
Description

Provide a centralized, versioned library of project-specific templates (e.g., fence, paint, roof, solar) configurable per community. Admins can create, edit, preview, publish, and roll back templates with effective dates. Templates define field sets (materials, colors, finishes, manufacturer links, required uploads), help text, and default values. Versioning ensures in‑flight submissions remain bound to the original template while new submissions use the latest. Supports community scoping, permissions, change logs, and export/import for reuse across properties. Integrates with Duesly’s feed, roles, and audit logging to standardize data capture and reduce clarification cycles.

Acceptance Criteria
Create and Preview Community-Scoped Template
Given I am a Community Template Admin And I select Community A When I create a new "Paint" template with fields: Materials (text), Colors (color-swatch), Finishes (enum), Manufacturer URL (link), Required Uploads (file, required) And I add help text and default values for applicable fields And I save as Draft And I click Preview Then the template is saved as Draft in Community A with a unique template ID and version 1 And the Preview renders all fields with help text and default values visible And the Manufacturer URL field validates http/https URLs and rejects invalid formats And required upload fields are indicated as required in the Preview
Publish Template Version With Effective Date
Given a Draft template version 1 exists for Community A When I schedule publication with an effective start date/time T Then the version status becomes Scheduled if T is in the future, or Published if T <= now And before T, submitters cannot select this version And at or after T, new submissions automatically use this published version And the template library shows version number and effective date/time
In‑Flight Submissions Bound To Original Version
Given Submission S was started using template "Paint" version 1 And version 2 is published with effective date/time after S was started When the submitter edits and resubmits S Then S continues to display and validate against version 1 And new submissions created after version 2 is effective use version 2 And the submission record stores the template version ID used
Rollback To Prior Template Version
Given template "Paint" has versions 1 (Published) and 2 (Published) When I roll back to version 1 with a rollback reason "Incorrect finish options" Then version 1 becomes the active Published version for new submissions And version 2 is no longer active for new submissions and remains viewable in version history And in‑flight submissions remain bound to their originally selected versions And the change log records the rollback action with actor, timestamp, from-version, to-version, and reason
Permissions And Community Scoping
Given I have the Template Admin role in Community A and no such role in Community B When I access the Template Library Then I can create, edit, preview, publish, roll back, export, and import templates for Community A And I cannot modify templates in Community B; I can only view Published versions if my role allows view-only access And non-admin users cannot access template authoring actions in any community And API and UI enforce the same permission checks
Audit Logging, Change Log, And Feed Integration
Given I perform template lifecycle actions (create, edit, publish, schedule, roll back, import) in Community A When each action completes Then an immutable audit record is stored with actor, action, timestamp, community, template ID, version, and field-level diffs for edits And the Template change log UI displays these records in chronological order with filtering by action and version And a Duesly feed entry is posted for publish and rollback events with template name, version, effective date/time, and a link to the template preview, visible to Board and Admin roles only
Export And Import Templates Across Properties
Given a Published template "Fence" version 3 exists in Community A When I export the template Then a portable file (e.g., JSON) is downloaded containing the field schema, help text, defaults, and validations And when I import this file into Community B Then the system validates field types, required flags, and URL formats, and prompts to map any community-specific enumerations And on validation success, a new Draft template is created in Community B with a new template ID and version 1 And on validation failure, no changes are persisted and I see a list of blocking errors
Dynamic Spec Form & Validation
"As a homeowner, I want guided, adaptive forms that only ask me relevant questions so that I can complete accurate submissions without back-and-forth."
Description

Render forms dynamically from the selected template with field types (text, number, select, multiselect, URL, color, file/photo), conditional logic, and real-time validation. Enforce required fields, value ranges, pattern checks (e.g., color codes, SKU formats), and URL/domain validation for manufacturer references. Provide contextual guidance, inline errors, and per-field help text. Include autosave drafts, mobile-first layout, and accessibility compliance. Persist partially completed forms; support resuming across devices. Integrates with rules engine and swatch capture to reduce ambiguity and ensure completeness before submission.

Acceptance Criteria
Render Form From Selected Template
Given a user selects the "Fence" spec template When the Dynamic Spec Form loads Then the form renders all fields defined by the template with correct field types (text, number, select, multiselect, URL, color, file/photo) And field order matches the template definition And default values and placeholders from the template are displayed And per-field help text is visible where configured And no fields outside the template are rendered
Conditional Logic Shows/Hides Fields and Applies Validation
Given a template with conditional logic where selecting Material = "Wood" shows Stain Color and Grain Direction When the user selects Material = "Wood" Then the dependent fields become visible and interactive within 200 ms And only visible dependent fields are required and validated When the user changes Material to a value that does not meet the condition Then the dependent fields are hidden And their values are cleared unless marked persist-on-hide in the template And hidden fields do not trigger validation errors or block submission
Real-Time Validation, Inline Errors, and Field Help
Given a user is editing any field When the input violates a validation rule (e.g., number outside 0–120, color not matching ^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$) Then validation triggers within 300 ms after blur or 500 ms after last keystroke And the field is marked invalid with aria-invalid=true and an inline error message describing the failed rule And the error message is linked via aria-describedby and is announced by screen readers And the error disappears within 200 ms after the input becomes valid while help text remains visible
Submission Gate Enforces Completeness and Rules Engine Checks
Given all required and visible fields are completed and local validation passes And rules engine policies for the selected template are available When the user taps Submit Then the submit button enters a loading state and is disabled until completion And the submission is blocked if any rules engine check fails, returning structured errors mapped to specific fields And no submission is accepted unless required swatch capture fields include at least one attached swatch or valid color value And a successful submission returns HTTP 201 with a formId and server timestamp
URL and Domain Validation for Manufacturer References
Given a field is configured as Manufacturer URL with allowed domains ["*.sherwin-williams.com","*.behr.com","*.owenscorning.com"] When the user enters a URL Then the URL must be syntactically valid (HTTP/HTTPS) and the registrable domain matches the allowed list And URLs without protocol are auto-prefixed with https:// and revalidated And shortened or redirecting URLs are rejected if the final domain is not on the allowed list And invalid entries show inline error "Enter a valid manufacturer URL from an approved domain"
Autosave Drafts and Resume Across Devices
Given a partially completed form When the user stops typing for 2 seconds or navigates away Then the form autosaves a draft with version number and updatedAt timestamp And the draft persists if the user closes the app or switches devices When the user signs in on another device and opens the same spec form Then the most recent draft restores within 2 seconds and the user can continue editing from the last cursor position And field values, selected options, and file/photo placeholders are restored without duplication
Mobile-First Layout and Accessibility Compliance
Given the form is opened on a mobile viewport (320–480 px width) When the user scrolls and edits fields Then all interactive targets are at least 44x44 px and the layout requires no horizontal scrolling And color and file/photo inputs present touch-optimized pickers And the experience meets WCAG 2.2 AA via automated and manual checks: focus order is logical, text contrast >= 4.5:1, components operable by keyboard, and controls have accessible names via labels/aria
Swatch Capture & Color Picker
"As a homeowner, I want to attach accurate color swatches easily so that reviewers can see exactly what I’m proposing without ambiguity."
Description

Enable quick capture of color and finish swatches via device camera, photo upload, or color picker. Extract and store color values (HEX/RGB), display thumbnails, and allow annotating swatches with manufacturer names and codes. Support multiple swatches per submission, basic image processing (compression, orientation fix, EXIF strip), and size/type validation. Provide contrast previews and standardized sample previews (e.g., fence panel, roof tile) for reviewer clarity. Ensure secure storage and fast thumbnail delivery for the review workflow.

Acceptance Criteria
Capture swatch via device camera
Given the user is on the Spec Picker Swatch Capture step with camera permission granted When the user taps "Capture with Camera" and confirms a photo Then the app stores the original image compressed to ≤ 4 MB, corrects orientation, strips all EXIF metadata, and generates a thumbnail ≤ 512 px longest side and ≤ 150 KB Given the captured image is shown When the user taps the eyedropper on the image and selects a point Then the sampled color values are displayed as HEX (#RRGGBB) and RGB (R,G,B) in sRGB and persisted with the swatch Given normal network conditions (≥ 1 Mbps) When the photo is saved Then the thumbnail appears in the swatch list within 1 second at P95; on failure the system retries up to 3 times and then shows a clear error state
Upload existing photo as swatch
Given the user chooses "Upload Photo" When a file is selected Then only JPEG, PNG, or HEIC files ≤ 20 MB are accepted; HEIC is auto‑converted to JPEG; other types or oversize files show an inline validation message and prevent upload Given an upload is in progress When transfer is occurring Then a progress indicator shows percentage and a cancel option is available Given the upload completes successfully Then orientation is corrected, EXIF is stripped, the stored original is compressed to ≤ 4 MB, and a thumbnail ≤ 512 px and ≤ 150 KB is generated and displayed; the image is available for color sampling Given assets are stored Then originals and thumbnails are encrypted at rest; access uses signed, expiring URLs (≤ 15 min); unauthorized requests return 403; P95 thumbnail load time in target region is ≤ 800 ms (first view) and ≤ 300 ms (cached)
Pick color via digital color picker
Given the user selects "Pick a Color" When the color picker opens Then it provides a hue slider, saturation/brightness area, and HEX and RGB input fields; inputs validate format and normalize HEX to uppercase #RRGGBB Given a color is selected or entered Then HEX and RGB values are stored in sRGB, displayed next to the swatch thumbnail, and copy‑to‑clipboard is available for each value Given invalid input (e.g., non‑hex chars, RGB outside 0–255) Then the field shows an inline error and Save is disabled until corrected
Manage multiple swatches in one submission
Given the user is adding swatches When swatches are added Then the form supports 1–10 swatches per submission and shows each as a card with thumbnail and fields Given multiple swatches exist When the user reorders or deletes a swatch Then order updates immediately without data loss; deleting asks for confirmation and removes the associated image and values Given the user leaves the form and returns later When the draft is reopened Then all swatches, images, color values, annotations, and order are restored exactly as saved
Annotate swatches with manufacturer and code
Given a swatch exists When the user enters Manufacturer Name and Product Code Then values are validated (Manufacturer: 1–100 chars; Product Code: 1–60 chars; allowed: letters, numbers, spaces, -, _, /) and saved with the swatch Given the active Spec template marks Manufacturer and Product Code as required When the user attempts to submit Then submission is blocked until required fields are completed for each swatch, with inline error highlighting Given multiple swatches exist When annotations are saved Then annotations are displayed in the reviewer feed and included per‑swatch in the submission payload
Contrast preview and accessibility checks
Given a color value exists for a swatch When the user opens Contrast Preview Then the system shows the color against light (#FFFFFF), dark (#000000), and any template‑defined background color, with WCAG 2.1 contrast ratios computed Then AA/AAA pass/fail badges are shown for normal (4.5:1) and large text (3:1), with sample text overlays Given the user changes the color value When the value updates Then the contrast preview updates within 200 ms
Standardized sample previews for reviewer clarity
Given a swatch exists When the user selects a sample type Then the system renders the color on at least two standardized samples: Fence Panel and Roof Tile, at a minimum resolution of 800×600 with consistent lighting/shading Given a preview is visible When the user toggles between sample types Then the render switches within 300 ms and preserves the selected color Given the submission is saved When the review feed loads Then the selected sample preview thumbnails are generated and attached to the submission for reviewers
Manufacturer Link & Autofill
"As a homeowner, I want to link the exact product and have key fields auto-filled so that my submission is precise and faster to complete."
Description

Allow users to link directly to manufacturer/product pages and attempt metadata retrieval (title, product name, color code, finish, image) for autofill. Validate URLs, enforce allowlists/denylists, and cache fetched metadata with snapshots to protect against link rot. Provide manual override when autofill is incomplete. Flag broken or unsupported links and suggest alternatives. Maintain a community-level catalog of frequently used products for quick selection. Integrates with validation and rules engine to ensure linked products meet CC&R constraints.

Acceptance Criteria
Valid Manufacturer URL Autofill in Spec Picker
Given I am creating a spec in Spec Picker and paste a manufacturer product URL from an allowed domain When I click Fetch Metadata Then the system validates the URL format (HTTP/HTTPS) and domain And fetches metadata within 5 seconds with a single request and no more than 1 retry And auto‑populates fields it can resolve: Product Name or Title (at least one required), Manufacturer, Color Code (if present), Finish (if present), and a primary Image preview And indicates which fields were auto‑filled And leaves untouched any fields the user has already manually entered
URL Allowlist/Denylist Enforcement
Given the community has a configured allowlist and denylist for manufacturer domains When the user enters a product URL Then the system blocks fetch if the domain is on the denylist or not on the allowlist (when allowlist enforcement is enabled) And displays an inline error stating the reason: Unsupported or Denied Domain And prevents submission until a compliant URL or manual product selection is provided
Manual Override and Non‑Destructive Re‑fetch
Given fields were auto‑filled from a manufacturer URL When the user edits any auto‑filled field Then that field is marked as Manually Overridden And a subsequent Re‑fetch Metadata action does not overwrite manually overridden fields And if the user explicitly selects Overwrite Manual Edits, only then are those fields replaced by the latest fetched values
Metadata Snapshot and Link‑Rot Protection
Given metadata and image(s) were successfully fetched Then the system saves an immutable snapshot (normalized fields and a stored image file) with a timestamp When the original URL later returns 4xx/5xx or times out (>5 seconds) Then the snapshot is used to display product details and the link is flagged as Broken And the UI shows Snapshot as of <date> and allows a manual refresh attempt And automatic re‑fetch attempts occur no more than once every 7 days unless manually triggered
Broken/Unsupported Link Handling with Alternatives
Given a user enters an invalid, unsupported, or unreachable product URL When fetch is blocked or fails after retry Then the system displays a clear error and offers up to 5 suggested alternatives from the community catalog based on matched manufacturer name, model keywords, or color code And selecting an alternative populates the spec fields from the catalog entry without requiring a URL
Community Catalog Creation and Quick Select
Given a spec with a valid manufacturer link is saved When the product is not already in the community catalog (deduplicated by Manufacturer + Product Code/Model + Color Code when available) Then the product is added to the catalog with normalized fields, thumbnail image, and searchable keywords And it appears in Quick Select/typeahead results within 1 minute for all board members/managers of the community
CC&R Compliance Validation on Linked Products
Given the spec contains linked product metadata and/or manual overrides When the user attempts to submit the spec Then the rules engine validates against CC&R constraints for the project type (e.g., allowed color families, finishes, manufacturers) And if violations exist, submission is blocked and specific failing rules are listed with field‑level highlights And if compliant, submission succeeds and the compliance result is recorded with a reference to the snapshot/version used
One-click Spec Request from Feed
"As a manager, I want to convert a resident’s post into a structured spec request so that we can formalize and track it without duplicating effort."
Description

Add an action in the Duesly feed composer and on existing posts to start a Spec Picker request in one click. Pre-populate property/lot, contact, and project type from context; attach the original post for traceability. Enforce permissions and notify the appropriate ARC/reviewer group. Create a linked feed item that tracks submission status, shows required next steps, and triggers automated reminders for missing information. Minimizes retyping and keeps conversations and formal requests connected in a single stream.

Acceptance Criteria
One-Click Action in Composer and Posts
Given an authenticated user with permission to initiate Spec Requests When they open the feed composer Then a "Request Specs" action is visibly available and enabled Given an authenticated user with permission to initiate Spec Requests and an existing feed post they can act on When they open the post actions menu Then "Request Specs" is present and enabled Given the user clicks "Request Specs" from either location When the Spec Picker loads Then the Spec Picker starts within 2 seconds and focuses the first required field
Context Pre-Population from Post/Composer
Given a source post contains property/lot, contact, and project type metadata When the Spec Picker opens from that post Then those fields are pre-populated with the post's values and are editable before submission Given the composer is scoped to a property/lot or contact When the Spec Picker opens from the composer Then those scoped values are pre-populated; any missing required value prompts inline validation and blocks submission until provided Given pre-populated fields are displayed When compared against community reference data (properties, contacts, project types) Then values match existing records exactly (IDs and display labels)
Original Post Attachment for Traceability
Given a Spec Request is initiated from a feed post When the request is created Then the request stores the source post ID and displays a "View Original Post" link on the request detail and linked feed item Given the request references a source post When user permissions differ between the request and source post Then the link respects source post access; users without access see an access-limited message without breaking the request record Given the source post is deleted or archived When viewing the request Then the request retains a non-null reference and shows a "Post unavailable" state while preserving audit trail
Permission Enforcement and Access Control
Given a user without the "Initiate Spec Request" permission When they view the composer or a post actions menu Then the "Request Specs" action is not visible Given a user attempts to access the Spec Request creation URL directly without permission When the request is made Then a 403 response is returned with an error code and the event is audit-logged with user ID, timestamp, and route Given a user with permission selects a property/lot When they attempt to submit for a property/lot they are not authorized for Then submission is blocked with a validation error and is audit-logged
ARC/Reviewer Notification Routing
Given a Spec Request is submitted When routing rules evaluate project type and community configuration Then notifications are sent to the correct ARC/reviewer group within 60 seconds via in-app and email, including property, project type, submitter, and links to the request and original post Given notification delivery fails When the first attempt errors Then the system retries up to 3 times with exponential backoff and surfaces a non-blocking alert to the submitter; all attempts are logged Given a reviewer receives a notification When they open it Then they land on the request detail with context and can change status per their permissions
Linked Feed Item with Status and Next Steps
Given a Spec Request is submitted When creation completes Then a linked feed item is created that is visible to authorized viewers and displays current status and a next-steps checklist Given the request status changes (Draft, Submitted, In Review, Needs Info, Approved, Denied) When viewing the linked feed item Then the status reflects the change within 5 seconds and a timeline entry is added with timestamp and actor Given a next step is completed from the feed item When the user clicks "Complete Next Step" Then the corresponding task is marked done, progress updates on both the feed item and request, and the action is audit-logged
Automated Reminders for Missing Information
Given a request enters "Needs Info" with required fields incomplete When 72 hours elapse without completion Then an automated reminder is sent to the requester via in-app and email and a mention comment is posted on the linked feed item Given reminders are active for a request When required fields remain incomplete Then reminders repeat every 72 hours up to 3 total sends, pausing while the request is "In Review" or after "Approved" or "Denied" Given a requester opts out of reminders for a request When future reminder triggers occur Then no further reminders are sent and the opt-out is recorded in the audit log
CC&R Rules Engine & Compliance Checks
"As an ARC reviewer, I want inline compliance checks against our CC&Rs so that non-compliant details are corrected before the request reaches review."
Description

Provide a configurable rules engine that evaluates form inputs against community CC&Rs and design guidelines. Support allowlists (e.g., approved colors), numeric constraints (e.g., fence height), conditional prohibitions (e.g., certain materials near common areas), and geographic exceptions. Offer inline warnings, hard blocks, or required acknowledgments with links to the relevant clauses. Log violations, overrides, and reasons for audit. Enable per-template rule sets and test mode to validate new rules before publishing. Reduces non-compliant submissions and speeds review.

Acceptance Criteria
Approved Color Allowlists – Paint Template
Given the Paint Spec Picker template is selected with the community’s rule set active When the user opens the exterior color selector or enters a custom color value Then only allowlisted colors are selectable in the picker And entering a non-allowlisted value triggers an inline hard-block message with a link to the relevant CC&R clause And the Submit action is disabled until a compliant color is selected And the system logs userId, timestamp, ruleId, entered value, allowlistMatch, and outcome
Fence Height Numeric Constraint – Hard Block
Given the Fence Spec Picker template has a Max Height rule of 6 ft configured When the user enters a fence height greater than 6 ft in the height field Then the form displays a blocking error stating the maximum allowed height with a link to the CC&R clause And the Submit button remains disabled while the value is non-compliant And changing the height to 6 ft or less clears the error and enables submission And validation is enforced both client-side on change and server-side on submit
Material Prohibition Near Common Areas – Conditional Rule
Given the parcel intersects a 50 ft buffer around a designated common area per HOA GIS data When the user selects a prohibited material (e.g., chain-link) in the Fence Spec Picker Then the system shows a prohibition message with a link to the governing clause And submission is hard-blocked until the material is changed to an allowed option And selecting allowed materials (e.g., wood, wrought iron) does not trigger the prohibition And the event is logged with geofenceId, distance, ruleId, and chosen material
Corner-Lot Fence Exception – Required Acknowledgment
Given the lot is flagged as a corner lot with an exception limiting front-yard fence height to 4 ft When the user specifies a 5 ft front-yard fence in the Fence Spec Picker Then the system prompts a required acknowledgment explaining the exception with a clause link And the user must either adjust to 4 ft or check the acknowledgment to request a variance And if acknowledgment is checked, submission is allowed and the application is tagged "Needs Variance" And the acknowledgment event records userId, timestamp, ruleId, acknowledgment text, and variance flag
Per-Template Rule Set Isolation – Fence vs Roof
Given distinct rule sets are configured for Fence and Roof templates When the user switches between Fence and Roof Spec Picker templates Then only the rules assigned to the active template are evaluated And no rules from other templates are applied or shown And the system logs the templateId and ruleSetVersion used for each validation And submission uses the active template’s rule set consistently across client and server
Admin Test Mode – Validate Rules Before Publish
Given an admin creates or edits a rule set in the Rules Admin When the admin enables Test Mode for the rule set Then only admins can see and exercise the test rules in Spec Picker via a test session And end-user sessions continue to use the currently published rule set And the admin can run sample submissions to view pass/fail per rule with clause links and export a test report And publishing promotes the rule set to Live, increments version, and logs actor, timestamp, and change summary
Violation, Warning, and Override Logging – Audit Trail
Given a rule evaluation results in a warning, hard block, or required acknowledgment When a board member or manager overrides to proceed Then the system requires an override reason before allowing submission And the audit log records ruleId, submissionId, actorId, timestamp, originalValue, newValue (if applicable), outcome, and justification And audit entries are immutable and retrievable via admin export/API within 5 seconds And end users cannot view internal override reasons or comments
Submission Package & Review Workflow
"As an ARC reviewer, I want a complete, standardized package and tools to request revisions so that decisions are faster and well-documented."
Description

Assemble submitted data into a standardized review package with summary, swatches, product links, attachments, and compliance results. Generate a shareable PDF and a structured feed item. Provide reviewer tools: threaded comments, request-changes cycles, version diffs on resubmission, and approval/denial with templated messages. Track timestamps, decisions, and participants for audit. Integrate with Duesly notifications and reminders, and expose status to the submitter. Export approved specs for records and downstream vendors if applicable.

Acceptance Criteria
Standardized Review Package Assembly
Given a completed submission via Spec Picker, When a reviewer opens it, Then a review package is assembled containing summary fields, selected swatches, product links, file attachments, and automated compliance results. Given a required field is missing, When the package is assembled, Then the missing element is flagged and the package cannot be marked Ready for Review. Given product URLs are included, When assembling the package, Then URLs are validated for HTTP/HTTPS and rendered as clickable links. Given up to 20 attachments totaling ≤50 MB, When assembling the package, Then all attachments are included with file names and sizes. Given a submission ID, When the package is created, Then it is assigned a unique package ID and sections appear in the order: Summary, Materials, Colors/Swatches, Product References, Attachments, Compliance Results.
Shareable PDF and Structured Feed Item
Given a review package exists, When Generate PDF is invoked, Then a PDF is produced within 10 seconds containing all sections, applicant info, and timestamps, plus a QR/link to the online package. Given images and swatches are present, When generating the PDF, Then images render at ≥150 DPI and color codes display as HEX and descriptive names. Given product links are present, When generating the PDF, Then links are preserved as clickable hyperlinks. Given a share link is created, When accessed externally, Then access requires possession of the link and the link expires after 30 days by default (configurable). Given the package is created, When posting to the community feed, Then a structured feed item appears with title, status badge, last-updated timestamp, assignee, and action buttons (Open, Comment).
Reviewer Threaded Comments and Mentions
Given a reviewer adds a comment, When posted, Then it appears as a top-level thread with author and timestamp. Given a top-level comment exists, When another user replies, Then a nested reply is created and displayed in chronological order within the thread. Given a user types @ and selects a participant, When the comment is posted, Then the mentioned user receives an in-app notification and the mention is highlighted. Given comment visibility settings, When a comment is marked internal-only, Then submitters cannot view it; when marked public, submitters can view it. Given a comment is under 5 minutes old, When the author edits or deletes it, Then the action is allowed and logged; after 5 minutes, edits require admin and deletions are disallowed.
Request Changes Cycle and Version Diff
Given a reviewer identifies issues, When Request Changes is submitted using a template, Then the submitter receives the request with templated text and optional notes, and status changes to Changes Requested. Given the submitter updates the submission, When resubmitted, Then a new version vN+1 is created and prior versions become read-only. Given two versions exist, When viewing the diff, Then changed fields are highlighted, added/removed attachments are listed, and swatch/color changes show before/after values. Given status is Changes Requested, When a resubmission arrives, Then status transitions to Under Review and assigned reviewers are notified. Given more than 3 change cycles have occurred, When initiating another Request Changes, Then the system displays a confirmation warning before proceeding.
Approval/Denial, Messaging, and Export
Given review is complete, When Approve is selected, Then a templated approval message with dynamic fields (owner name, address, submission ID) is required and the decision, approver, and timestamp are recorded. Given a denial is appropriate, When Deny is selected, Then a templated denial message with a required rationale is required and the submitter is notified. Given a package is approved, When attempting to edit submission content, Then edits by the submitter are blocked and a read-only banner is shown. Given a package is approved, When Export is selected, Then the system generates (a) a PDF bundle (PDF plus attachments in a ZIP) and (b) a machine-readable JSON containing structured fields, links, and compliance outcomes. Given a vendor email is provided, When sharing the export, Then a secure download link is sent that expires after 14 days and requires a one-time code for access; file names follow [Community]_[Address]_[SubmissionID]_vN_[YYYYMMDD], and the export event is logged with actor, time, format, and recipient.
Comprehensive Audit Trail
Given any action occurs on a package, When the action is performed, Then an audit entry records actor, action, before/after values, timestamp (ISO 8601 UTC), and IP/device metadata. Given audit entries exist, When viewed by an admin, Then entries are immutable, filterable by date/user/action, and exportable to CSV and JSON. Given timestamps are displayed, When viewed in the UI, Then they show in the viewer’s local timezone with a hover detail for UTC.
Notifications, Reminders, and Status Visibility
Given a status change (Submitted, Under Review, Changes Requested, Resubmitted, Approved, Denied), When it occurs, Then the submitter sees real-time status updates in their submission view and corresponding feed item. Given reviewers are assigned, When no activity occurs for 3 business days, Then an automated reminder is sent to the assignee; after 5 business days, an escalation notification is sent to a manager. Given status is Changes Requested, When 7 days pass without resubmission, Then automated reminders are sent to the submitter every 3 days up to 3 times or until resubmission occurs. Given a user opts out of email, When notifications are sent, Then in-app notifications are still delivered and emails are suppressed.

Neighbor Acknowledgment

Request required adjacent‑owner acknowledgments via SMS/email with a concise project summary and thumbnails. Duesly tracks who’s signed, sends polite reminders, and logs consent for the packet—no door knocking or paper chasing.

Requirements

Adjacent Owner Resolver
"As a homeowner submitting a project that requires neighbor acknowledgment, I want Duesly to automatically identify my adjacent owners and preload their preferred contact channels so that I can send requests in seconds without errors."
Description

Automatically identifies required adjacent owners for a project using the community’s property roster and lot adjacency rules, preloading recipient records with names, roles (owner/tenant), preferred channels (email/SMS), language, and contact validations. Supports manual add/remove and overrides to handle edge cases (corner lots, shared walls, missing data). Writes a normalized recipient list into the project’s acknowledgment packet, de-duplicated across units and households, and synchronizes with Duesly’s member directory for ongoing accuracy. This reduces manual effort, prevents omissions, and anchors the rest of the flow on a reliable recipient set tightly integrated with Duesly’s property data model and permissions.

Acceptance Criteria
Auto-resolve adjacent owners from roster and adjacency rules
Given a project with a valid lot/parcel ID and an active community adjacency rule set When the Adjacent Owner Resolver runs Then it identifies all and only adjacent lots defined by the rule set (precision = 100%, recall = 100% on the test fixture graph) And for each adjacent lot it preloads a recipient with full name, role (owner or tenant), preferred channels (email/SMS), language, and contact validation flags And email addresses are marked valid only if they pass RFC/MX checks; phone numbers only if they pass E.164 formatting and SMS deliverability check And each recipient is linked to a stable directory entity ID (person_id or household_id) And the resolution completes in ≤ 2 seconds for communities up to 5,000 units
Normalize and deduplicate recipients across units and households
Given multiple adjacency hits point to the same directory person or household When the normalized recipient list is generated Then only one recipient per directory entity appears in the packet (no duplicate person_id or household_id) And duplicate contact endpoints (same email or phone) are collapsed to a single recipient And if one person owns multiple adjacent lots, the single recipient aggregates all adjacency reasons/lot IDs And if community policy requires separate consent per distinct role (e.g., owner and tenant), two recipients are retained; otherwise they are merged with owner prioritized
Manual add/remove and field overrides with audit trail
Given a user with Manage Acknowledgments permission When they manually add a recipient by searching the directory or entering freeform data Then the recipient is added with an override flag and a mandatory reason And when they remove a system-resolved recipient Then a reason must be selected and the recipient is excluded with an override flag And overrides immediately re-run deduplication to prevent duplicates And all add/remove/field-change actions are logged with timestamp, actor, before/after values, and reason
Channel validation and sendability readiness
Given a resolved recipient list with preferred channels When preferred channel is invalid or unverified Then the system auto-selects the next valid channel for that recipient according to priority (SMS > Email or as configured) And a packet cannot be marked Ready to Request unless 100% of recipients have at least one validated channel or an explicit Sendability Override is applied by an authorized user And the resolver outputs a summary: total recipients, sendable count, invalid-contact count, and missing-language count that matches the recipient list state
Real-time sync with member directory
Given recipients are linked to directory entities When directory data (name, role, channel preference, language, contact) changes Then the recipient list reflects the changes within 60 seconds without manual refresh And when a user edits recipient contact details via override Then updates are pushed back to the directory where permissions allow and flagged as user-sourced And if no high-confidence (≥ 0.95) match exists, a provisional directory entity is created and flagged for review And no fields outside the user's permission scope are modified in the directory
Permissions and data exposure controls
Given community role-based permissions When an unauthorized user attempts to resolve, add, remove, or override recipients Then the action is blocked with a 403 and logged And authorized users (Board/Manager) can perform all resolver actions And viewers without contact access can see names and roles but not emails/phones And exports or API responses respect field-level permissions for all recipient records
Edge-case adjacency handling: corner lots, shared walls, missing data
Given a corner lot with multiple street frontages When the resolver runs Then it includes all adjacent lots per the multi-frontage rule and records the adjacency reason per lot Given a shared-wall (townhome/condo) building When the resolver runs Then vertical and horizontal adjacency is computed per building topology, not just parcel polygons Given missing or incomplete geometry/roster data When the resolver runs Then it flags affected lots, applies configured fallback rules (e.g., building-level adjacency), and requires manual confirmation before marking Ready to Request
Write normalized recipient list into packet
Given a project acknowledgment packet in Draft status When the resolver completes successfully Then it writes a normalized recipient list to the packet with stable IDs, adjacency reasons, channel preferences, language, and validation flags And the list is versioned; each resolver or override run creates a new immutable version with diff metadata And the packet exposes the current version while retaining history for audit and rollback And downstream features (reminders, consent logging) can reference recipients by stable ID without re-resolution
Consent Request Composer & Delivery
"As a project initiator, I want to compose a clear acknowledgment request with thumbnails and send it via email/SMS using unique links so that neighbors can easily review and respond."
Description

Generates a concise, branded acknowledgment request with project summary, thumbnails, and optional attachments using reusable templates and merge fields. Creates unique, signed links per recipient and dispatches via email and/or SMS with channel fallback, rate limiting, and per‑message personalization. Provides preview and test-send, supports localized copy, and records delivery metrics (queued, sent, bounced) in the project feed. Integrates with Duesly’s notifications service and respects user communication preferences and unsubscribe rules to maximize deliverability and clarity.

Acceptance Criteria
Template-Based Composition & Personalization
Given a reusable template is selected and a project includes a summary, thumbnails, and optional attachments When the user composes a consent request for selected recipients Then the system generates email and SMS bodies per recipient with all merge fields (e.g., name, address, lot) resolved And applies community branding to email content And displays thumbnails inline in email and as short URLs in SMS And attaches files to email and includes secure download links in SMS And flags any unresolved/invalid merge fields with field names and blocks sending until resolved And segments SMS messages as needed to meet provider size limits without dropping required content
Unique Signed Links per Recipient
Given a consent request is being prepared for multiple recipients When signed acknowledgment links are generated Then each recipient receives a unique URL with a signed token bound to the project and recipient And the token has a configurable expiration (TTL) And accessing the link attributes the session to the correct recipient and project And tampered or expired tokens return an invalid/expired page without exposing project details And resending generates a new token and invalidates prior tokens for that recipient
Channel Selection, Fallback, and Preference Compliance
Given recipients have email/mobile contacts and saved communication preferences and unsubscribe states for Neighbor Acknowledgment When dispatching the consent request batch Then the system selects the highest-priority channel permitted by the recipient's preferences And skips any channel where the recipient is unsubscribed or suppressed And falls back to the alternate permitted channel if the preferred channel is unavailable, bounces, or is suppressed And if no permitted channel exists, no message is sent and a suppression reason is recorded per recipient
Rate Limiting and Queueing
Given a configured outbound rate limit L per channel per community and provider constraints When a send batch exceeds L Then messages beyond L are queued and released in FIFO order at or below L And no more than L messages are dispatched per time window per channel And per-message state transitions reflect queueing and dispatch times
Preview, Localization, and Test Send
Given localized copy exists for supported locales and a default locale is configured When the user selects a locale and opens the preview Then email and SMS previews render in the selected locale with fallback to default for any missing strings And merge fields render using selected recipient data or representative sample data And when the user performs a test send to a specified address/number, the message is delivered using the selected locale and is not sent to actual recipients
Delivery Metrics and Project Feed Logging
Given messages are dispatched via email and/or SMS When delivery receipts or webhooks are received Then each recipient's message status is updated to queued, sent, or bounced with timestamp, channel, and provider message ID And bounce or suppression reasons are captured per recipient And these events are appended to the project feed in chronological order and are filterable by status
Notifications Service Integration
Given Duesly’s notifications service is available When the composer dispatches messages Then the system uses the notifications service API with payloads including channel, recipient contact, personalized content, projectId, recipientId, and locale And stores returned service message IDs for correlation And if the API call fails, the message is not marked sent and an error is recorded for the recipient with a reason
Secure Acknowledgment Portal
"As a neighbor recipient, I want a secure, mobile-friendly page to review the project and acknowledge or decline with a valid signature so that my response is clear and auditable."
Description

Hosts a mobile-first, no-login recipient page that displays the project summary, images, and attachments with clear actions: Acknowledge, Decline, or Ask a Question. Captures typed name or signature, optional comments, and consent to receive updates; records tamper-evident metadata (timestamp, IP, user agent, link token) and supports link expiration and reissue. Enforces packet versioning so recipients always consent to a specific immutable version. Accessible (WCAG AA), localized, and compatible with low-bandwidth devices. Writes outcomes to an append-only audit log and updates project status in Duesly in real time.

Acceptance Criteria
Secure Tokenized Access & Link Expiration
- Given a recipient possesses a valid, unexpired link token, When they open the link on any device, Then the portal loads over HTTPS without requiring login and displays the packet. - Given a link token that is expired per configured TTL, When the recipient opens the link, Then the portal shows an expiration message and a "Request new link" action. - Given the recipient requests a new link, When the system issues a new token, Then all prior tokens for that recipient and packet version are invalidated and the new link is delivered via the original channel (SMS/email). - Given an invalid or reused token, When the link is opened, Then the portal denies access without revealing packet contents and logs the attempt.
Packet Rendering and Integrity
- Given a packet version, When the portal loads, Then it displays the project summary text, image thumbnails, and a list of attachments with file type and size. - Given images in the packet, When the page renders, Then images are lazy-loaded with placeholders and support tap-to-zoom on mobile. - Given attachments are present, When a recipient taps an attachment, Then the file downloads or opens in a new tab without blocking the portal. - Given packet metadata, When displayed, Then the immutable version ID and "Last updated" timestamp are shown on the page.
Consent Actions and Data Capture
- Given the portal is loaded, When the recipient views actions, Then Acknowledge, Decline, and Ask a Question controls are visible and keyboard-accessible. - Given the recipient chooses Acknowledge or Decline, When they proceed to submit, Then they must provide either a typed full name or a drawn signature, and may optionally add comments. - Given a consent submission, When the recipient checks "Consent to receive updates", Then the subscription preference is stored alongside the outcome. - Given a submission is in progress, When the user taps Submit, Then a confirmation summary is shown, the submit button is disabled to prevent duplicates, and on success a receipt screen with a reference ID is displayed.
Tamper‑Evident Audit Log, Metadata, and Real‑Time Status Update
- Given any outcome or question is submitted, When it is processed, Then an append-only log entry is created containing ISO8601 UTC timestamp, requester IP, user agent, link token ID, packet version ID, and a SHA‑256 hash of the submitted payload signed server-side. - Given the audit log, When an attempt is made to modify an existing entry via API, Then the system rejects the request and requires a new entry that references the prior entry for corrections. - Given a successful outcome submission, When the entry is committed, Then the related Duesly project counters and recipient status update within 2 seconds and are visible on the project feed. - Given a log entry exists, When an admin exports audit logs, Then the entry is included and its integrity is verifiable by recomputing hashes.
Packet Versioning and Re‑Consent Enforcement
- Given a recipient opens the portal, When a packet version is displayed, Then the version label is visible and the consent/outcome is bound to that exact version ID and hash. - Given a new packet version is published after a prior consent, When the recipient opens any old link, Then they are informed the packet has changed and must review and consent to the new version; the prior consent remains recorded but is not applied to the new version. - Given a recipient has already submitted for the current version, When they attempt to resubmit, Then the portal prevents duplicate outcomes for that recipient and version and offers to view their receipt.
Accessibility Compliance (WCAG 2.2 AA)
- Given a keyboard-only user, When navigating the portal, Then all interactive elements are reachable in a logical order with visible focus indicators and operable without time limits. - Given a screen reader user, When reading the page, Then controls have descriptive labels, images have meaningful alt text, and status changes (e.g., form errors, success) are announced via ARIA live regions. - Given contrast requirements, When rendered, Then text and interactive elements meet or exceed a 4.5:1 contrast ratio and touch targets are at least 44x44 CSS pixels. - Given validation errors, When a submission is attempted, Then errors are presented inline, associated with fields, and user-entered data is preserved.
Localization and Low‑Bandwidth Compatibility
- Given a recipient's Accept-Language header, When the portal loads, Then UI text is localized to a supported language (e.g., en, es) with a manual language switcher that persists the selection. - Given right-to-left languages, When selected, Then layout and typography adapt correctly (RTL) without layout breakage. - Given a 400 Kbps 3G network and a mid-range device, When the portal first loads, Then initial transfer size is ≤ 500 KB, first contentful paint occurs within 3 seconds, and core actions are available within 5 seconds. - Given small screens (≥320px width), When rendered, Then content is readable without horizontal scrolling and primary actions remain visible above the fold. - Given limited bandwidth, When images and attachments exist, Then they are not pre-fetched; thumbnails are ≤ 50 KB each and loaded lazily; the page remains usable if images fail to load.
Automated Reminder Cadence
"As a project initiator, I want Duesly to send polite, scheduled reminders until each neighbor responds so that I don’t have to chase people manually."
Description

Schedules polite, configurable reminders across channels until each recipient responds or a maximum cadence is reached. Supports smart send windows by timezone, escalating channel fallback, and suppression on response, bounce, or opt-out. Provides per-recipient cadence controls (pause, snooze, resume) and templated, board-approved copy. Logs open/click events where available and surfaces reminder activity in the project feed. Ensures a balance between effectiveness and neighbor goodwill while reducing manual follow-up by initiators and managers.

Acceptance Criteria
Stop Reminders on Recipient Response
Given a recipient has an active reminder cadence for a neighbor acknowledgment When the recipient submits an acknowledgment (approve or decline) via any supported channel or portal Then all future scheduled reminders for that recipient are immediately canceled within 60 seconds And the recipient’s cadence status is set to Completed-Responded And a feed entry is created with recipient, channel, timestamp, and response type And no additional reminders are sent unless a new request is initiated
Max Cadence Limit Reached Without Response
Given a maximum reminder attempts value per recipient is configured for the project And attempt counts are aggregated across all channels in the cadence When the recipient reaches the configured maximum without responding Then the cadence for that recipient is stopped And the recipient’s cadence status is set to Completed-Exhausted And a summary entry is posted to the project feed indicating total attempts by channel and final timestamp And no further reminders are scheduled for that recipient for this request
Timezone-Aware Smart Send Windows
Given the project has a configured local send window and minimum spacing between reminders And each recipient has a resolved timezone (profile, geocoded address, or project default) When a reminder would otherwise be scheduled outside the recipient’s send window Then it is deferred to the next available window in the recipient’s local time And daylight saving time rules are respected for that timezone And the minimum spacing constraint between reminders is enforced per recipient And the feed logs the deferment reason and new scheduled time
Escalating Channel Fallback
Given a primary channel and a fallback channel order are configured for the project And opt-out and bounce states are tracked per channel When a reminder send fails (bounce/undeliverable) on a channel or remains unengaged beyond the configured threshold Then the next channel in the fallback order is attempted within the next eligible send window And channels with opt-out, hard bounce, or invalid status are skipped And only one reminder is sent per escalation step And each escalation attempt is logged with channel, result, and timestamp
Per-Recipient Pause, Snooze, and Resume Controls
Given a user with permissions selects Pause or Snooze for a specific recipient’s cadence When Pause is applied Then no reminders are sent until Resume is triggered And all scheduled jobs are suspended and marked Paused When Snooze is applied with a defined duration Then reminders are suspended until the snooze expires, after which scheduling resumes at the next step within the send window And Resume maintains the step index (no duplicate sends) and respects max attempts And all control actions (Pause, Snooze, Resume) are logged in the feed with actor, reason (if provided), and timestamps
Template Governance and Personalization
Given board-approved reminder templates with required placeholders are selected for the project When a reminder is generated for a recipient Then the locked template version is used and populated with recipient name, property identifier, project title/summary, and reply/opt-out instructions And sending is blocked with a visible error if any required placeholder cannot be resolved And channel-specific constraints are enforced (e.g., subject required for email; link shortener applied for SMS) And the template version ID and personalization variables are recorded with the send event
Open/Click Tracking and Project Feed Visibility
Given tracking is enabled and supported for the selected channel When a reminder is delivered to a recipient Then open and/or click events are captured where technically supported (email opens/clicks; SMS clicks via tracked links) And events are attributed to the recipient and specific reminder attempt And the project feed shows per-recipient last activity (sent, delivered, opened, clicked, responded) with timestamps and channel And aggregated metrics (attempts, deliveries, opens, clicks, responses) are available at the project level And if tracking is unavailable or blocked, the send is not retried solely for tracking and a non-blocking note is logged
Acknowledgment Dashboard & Export
"As a project initiator or manager, I want a dashboard that shows who has opened, signed, declined, or bounced and lets me resend or export an audit packet so that I can track progress and report to the board."
Description

Presents a real-time dashboard within the project showing each recipient’s state (pending, delivered, opened, acknowledged, declined, bounced), last activity, and channel history. Enables actions such as resend, change channel, edit recipient details (with audit), and add manual acknowledgment received offline. Generates a downloadable consent packet (PDF/ZIP) including the request, signatures, timestamps, comments, and delivery log, plus a CSV for spreadsheets. Provides summary badges in the Duesly feed and notifications when thresholds are met (e.g., all required neighbors acknowledged).

Acceptance Criteria
Real-Time Recipient State Tracking on Project Dashboard
Given an acknowledgment request has been initiated for a project When the dashboard loads Then each recipient appears with an initial state of "pending" Given the system receives a provider event for a recipient indicating delivery When the dashboard refreshes or auto-updates Then the recipient state displays "delivered" and "Last activity" shows the delivery timestamp Given the system receives an open event for a recipient Then the state displays "opened" and "Last activity" shows the open timestamp Given the system records an acknowledgment for a recipient (online or manual) Then the state displays "acknowledged" and "Last activity" shows the acknowledgment timestamp Given the system records a decline for a recipient Then the state displays "declined" and "Last activity" shows the decline timestamp Given the provider reports a bounce for a recipient Then the state displays "bounced" and "Last activity" shows the bounce timestamp Given any state change event is received Then the per-state counts and progress indicator update within 10 seconds and persist after page refresh
Recipient Channel History and Last Activity Display
Given a recipient has one or more delivery attempts across SMS and/or Email When the user opens the recipient details panel Then a chronological channel history is shown with attempt number, initiator (Auto/User), channel (SMS/Email), outcome (delivered/opened/bounced), and timestamp for each attempt Given a new attempt occurs Then it is appended to the top of the channel history and "Last activity" reflects that attempt Given there are no attempts yet Then the channel history indicates "No attempts yet"
Resend and Change Channel Actions
Given a recipient is not in the "acknowledged" state When the user clicks "Resend" and selects a channel Then a new delivery attempt is sent via the selected channel, the attempt is logged in channel history, and "Last activity" updates with the attempt timestamp Given a resend has just been triggered for a recipient Then the UI prevents another resend for that recipient for 60 seconds and displays a rate-limit message Given the user opens "Change channel" for a recipient When the user selects SMS or Email and the contact value for that channel passes validation Then the preferred channel is updated, persisted, reflected in the recipient details, and an audit entry is recorded Given the user attempts to set a preferred channel with an invalid or missing contact value Then the change is blocked and inline validation errors are shown
Edit Recipient Details with Audit Trail
Given a user with edit permissions opens "Edit recipient" When the user modifies name, email, and/or phone and provides a required reason Then the save succeeds only if email and phone values are valid formats and required fields are present Then an audit record is created capturing user, timestamp, changed fields with old and new values, and the provided reason, and is visible in the recipient’s audit log Given a recipient is in "acknowledged" state When email or phone is edited Then the existing acknowledgment record remains intact and linked; the state remains "acknowledged" Given required reason is omitted or validation fails Then the save is blocked with field-level error messages
Record Manual Offline Acknowledgment
Given a recipient is not yet acknowledged When the user selects "Add manual acknowledgment" and enters method (e.g., in-person), acknowledgment date/time (not in the future), and optional comment/attachment Then upon save the recipient state changes to "acknowledged", the provided timestamp is stored, and a manual entry appears in channel history and the audit log Given a manual acknowledgment has been recorded for a recipient Then "Resend" is disabled for that recipient and the last activity reflects the manual acknowledgment time Given the entered acknowledgment date/time is in the future or missing required fields Then the save is blocked with inline validation errors
Export Artifacts (Consent Packet PDF/ZIP and CSV)
Given a project has at least one recipient When the user clicks "Export" Then the system offers options "Consent packet (PDF/ZIP)" and "CSV" and begins generation upon selection Given "Consent packet (PDF/ZIP)" is selected Then a downloadable file is produced within 60 seconds containing the request summary, project metadata, per-recipient sections (name, required/optional flag, status, signatures if acknowledged, timestamps, comments), and a delivery log of all attempts with channel and outcomes Given "CSV" is selected Then a UTF-8 CSV is produced with headers: recipient_id, name, address, required, preferred_channel, status, last_activity_at, first_delivered_at, first_opened_at, acknowledged_at, declined_at, bounced_at, attempts_count, ack_method, comment; timestamps are ISO 8601 UTC; the number of rows equals the number of recipients Given an export completes Then the file downloads successfully and its counts by status match the dashboard counts Given an export fails Then the user sees an error with a retry option
Feed Summary Badges and Threshold Notifications
Given a project has recipients When the dashboard updates Then the Duesly feed shows badges for counts by state (pending, delivered, opened, acknowledged, declined, bounced) and overall acknowledged percentage Given all required recipients are acknowledged Then a "All required acknowledged" badge is shown and a single notification is sent to the project owner and watchers via in-app and email Given a recipient changes to "declined" Then a "Declined" badge count increments and a notification is sent once for that recipient’s first decline Given any acknowledgment (online or manual) occurs Then badges and percentages update within 10 seconds
Compliance & Data Governance
"As a board admin, I want acknowledgments to be access-controlled, versioned, and retained with an immutable audit log so that the community meets legal and policy requirements."
Description

Implements role-based access controls limiting visibility of recipient PII to authorized roles, with masked views in the community feed. Applies retention policies and secure storage for signatures and audit logs, provides packet versioning and immutable event trails, and supports consent revocation or correction with full traceability. Ensures e-sign compliance, honors GDPR/CCPA data subject requests, and provides redaction for exports when required. Exposes webhook/events for downstream compliance modules so ARC reviews and approvals can be gated on required acknowledgments being met.

Acceptance Criteria
RBAC PII Masking in Neighbor Acknowledgment Feed
Given a user without PII privileges views Neighbor Acknowledgment items in the community feed, when recipient details are rendered, then names show as first+last initial, addresses exclude street numbers, emails/phones are obfuscated, and signature thumbnails are blurred. Given a user with PII privileges views the same items, then full recipient PII and clear signature thumbnails are visible. Given an API client without scope pii:read requests acknowledgment data, when the response is returned, then PII fields are omitted or masked and the request is logged; if explicit fields are requested, a 403 is returned. Given any user, when they attempt to bypass masking via URL parameters or query filters, then the system ignores such parameters for masked roles and returns masked data. Given any view of masked/unmasked data, when the response is served, then an audit event records actor, role, scope, resource id, fields masked/unmasked, and timestamp.
Secure Storage and Retention of Signatures and Audit Logs
Given a retention policy of 7 years for signatures and 10 years for audit logs is configured, when an item reaches its retention end, then it is cryptographically erased within 24 hours and a deletion event with item id, hash, and timestamp is appended to the immutable log. Given signatures are stored, then they are encrypted at rest with AES‑256‑GCM using KMS-managed keys; keys rotate at least every 90 days and rotation events are recorded. Given any access to signature binaries, when the requester lacks the signatures:read scope, then access is denied with 403 and an audit event is recorded; direct database or object-store reads return ciphertext only. Given a disaster-recovery restore, when restored data contains items already deleted per retention, then those items remain unrecoverable and a restore audit event notes exclusions. Given configuration changes to retention, when saved, then changes are versioned with actor, before/after values, and effective date; retroactive shortening does not delete earlier than legally allowed.
Packet Versioning and Immutable Event Trail
Given a Neighbor Acknowledgment packet is edited (recipients, summary, media), when changes are saved, then a new version is created with an incremented semantic version and prior versions are read-only. Given any packet lifecycle action (create, send, remind, sign, revoke, correct), when it occurs, then an append-only event is written with monotonic sequence id, UTC timestamp, actor, affected version, and content hash; each event includes prevHash to form a verifiable chain. Given the event stream is queried, when hash chain verification runs, then all events validate or the API returns a 409 IntegrityError. Given the versions endpoint is called, then it returns all versions with createdBy, createdAt, and a machine-readable diff summary; attempts to update prior versions return 409 Conflict. Given database maintenance or admin tools, then no operation exists to delete or overwrite past events; attempts are rejected and audited.
Consent Revocation and Correction with Full Traceability
Given a recipient previously signed, when they click the revoke link or submit a verified revocation request, then the consent state changes to Revoked within 5 minutes, all dependent processes stop, and the packet owner is notified. Given a revocation, then the original signature artifact remains preserved; a revocation event records reason, actor (self/support), timestamp, source ip, and evidence reference. Given a correction request (e.g., email typo), when verified, then the display fields are corrected for future communications while the original values remain in the audit trail marked as superseded; no historical artifact is altered. Given consent state changes (revoked or corrected), when webhooks are enabled, then consent.revoked or consent.corrected events are delivered within 60 seconds with redacted payloads unless the endpoint has pii:read scope.
E‑Sign Compliance (ESIGN/UETA) Flow
Given a recipient opens a signing session, when presented with the electronic records disclosure, then they must affirm consent via a separate checkbox before any signature control is enabled; declining blocks signing with a clear message. Given a signature is applied, then the system binds the signature to a document hash and records UTC timestamp, IP address, and user-agent; a tamper-evident receipt is generated and made available to both parties. Given time synchronization, then server timestamps are NTP-synced and recorded in UTC with <=2s drift in monitoring. Given any change to the document after signing, when validation runs, then the tamper check fails and the document is flagged Invalid with an audit event. Given multi-factor optionality is enabled by the community, when configured, then recipients must pass OTP (SMS/email) before accessing the signing page; failures are logged.
GDPR/CCPA Data Subject Requests for Neighbor Acknowledgments
Given a verified data subject access request, when processed, then the system produces an export within 30 days listing personal data related to the subject for Neighbor Acknowledgment, with other parties' PII redacted and an audit receipt provided. Given a verified deletion request where no overriding legal basis exists, when processed, then the system deletes or irreversibly anonymizes the subject's personal data, preserves minimal compliance metadata (legal basis, timestamps), and logs the action; related packets show anonymized placeholders. Given a verified rectification request, when applied, then current records are updated, prior values retained in the audit trail as superseded, and future exports reflect corrected data. Given any data subject request, then identity verification requires two-factor or a signed, time-limited link; unverified requests are rejected with a reason and are not fulfilled. Given exports are generated, then they include a redaction manifest describing fields omitted or masked and the legal basis for redaction.
Redacted Exports and Scoped Webhooks for ARC Gating
Given a user without PII privileges generates an export (CSV/PDF/JSON) for acknowledgments, then recipient fields are masked per policy and the export includes a redaction manifest; users with PII privileges may request unredacted exports only after providing a reason that is audited. Given webhooks are configured, when events are emitted, then payloads exclude PII by default and include PII only for endpoints registered with pii:read scope; all deliveries are signed and include an idempotency key. Given ARC gating queries the API, when required acknowledgments are unmet, then the endpoint returns status=Blocked with specific missing neighbor ids; when met, status=Ready and gate opens. Given webhook delivery failures, then retries use exponential backoff for up to 24 hours; persistent failures create an alert and are visible in an admin dashboard with last error. Given export or webhook data leaves the system, then a data lineage record is created capturing destination, scope, fields included, and retention on the destination if provided.

Readiness Check

Real‑time completeness and policy validation flags missing photos, map traces, dimensions, or off‑policy selections before submission. Clear fixes and inline tips help applicants self‑correct, resulting in fewer rejections and faster approvals.

Requirements

Real-time Completeness Validation
"As a homeowner submitting an architectural request, I want missing items to be flagged as I fill out the form so that I can fix issues immediately and avoid a rejection."
Description

Provide on-blur and on-change validation across application forms to detect missing required inputs (photos, dimensions, map trace, material selections), highlight missing elements inline with clear error states, and update in real time without page refresh. Integrate with Duesly form components and the submission API. Must support per-template required fields, conditional requirements (e.g., dimensions required when "Structure" = "Fence"), and mobile-friendly interactions. Expected outcome: fewer incomplete submissions, faster user self-correction, and reduced back-and-forth with boards.

Acceptance Criteria
On-Blur and On-Change Required Field Validation
Given a required input rendered via Duesly form components, when the user changes the value and blurs focus, then validation runs client-side and, if empty or null, an inline error "This field is required" appears, aria-invalid="true" is set, and error styling is applied within 300ms without a page refresh. Given a required field is in error, when the user enters a valid value, then the error message and styling clear and aria-invalid="false" within 300ms. Given a required select or toggle, when the user changes it to a valid non-default option, then any prior required-field error clears within 300ms.
Conditional Requirements: Dimensions when Structure=Fence
Given the template defines Dimensions (Height, Length) as required when Structure="Fence", when the user selects Structure="Fence", then Height and Length become visible and are marked required. Given Structure="Fence", when Height or Length is empty on blur, then the specific empty field shows an inline error "Dimensions required for fences" within 300ms and submission is blocked. Given Structure is not "Fence", then Dimensions are optional, show no required markers, and do not block submission.
Required Photo Upload Enforcement
Given the template marks Photos as required with a minimum of 1, when the user attempts to submit with 0 photos, then the Photos control shows an inline error "At least 1 photo is required" within 300ms and submission is blocked. Given the Photos control is in error, when the user uploads at least 1 photo successfully, then the error clears within 300ms and the photo count displays correctly. Given a required Photos control, when the user exits the control without any uploads, then the inline error appears within 300ms without page refresh.
Map Trace Presence and Shape Validation
Given the template requires a map trace, when the user exits the map editor without saving a geometry, then an inline error "Map trace is required" appears within 300ms. Given a map trace is required, when the user saves a geometry, then it must be valid GeoJSON of the allowed type per template (LineString ≥ 2 vertices, Polygon ≥ 3 vertices); otherwise show "Invalid trace shape" and keep the field in error. Given a valid geometry is saved, then the error clears within 300ms and the geometry payload is attached to the draft for submission.
Inline Error States and Mobile-Friendly Fix Guidance
Given any field enters an error state, then a concise inline message and error icon appear, and the first errored field receives focus or scrolls into view within 500ms upon submit. Given a mobile viewport (≤414px width), then error tap targets are ≥44x44 px, messages wrap without overlap, and the focused errored input is scrolled into view above the on-screen keyboard. Given keyboard or assistive tech navigation, then errors are associated via aria-describedby and announced via an aria-live polite region.
Template-Driven Required Fields and Conditional Logic
Given a template JSON defines required fields and conditional rules, when the form renders, then required indicators and validation behavior reflect the template without code changes. Given the template is updated server-side and fetched on form load, then the loaded rules govern validation for that session and are applied on-change and on-blur in real time. Given two template rules conflict, then the most specific conditional takes precedence and a deterministic outcome is applied consistently across reloads.
Submission Blocking and API Error Mapping (No Page Refresh)
Given any required field is incomplete, when the user clicks Submit, then the client prevents submission, highlights all invalid fields inline within 500ms, and the page does not refresh. Given client validation passes but the Submission API responds 422 with field errors, then the client maps each error to its field, shows inline messages within 500ms, and preserves all user inputs. Given all required data is complete and the API responds 201, then the success state is shown without page refresh and no client-side errors remain.
Policy Rules Engine (Off‑Policy Detection)
"As a board reviewer, I want applications to be checked against our community’s policies automatically so that only compliant requests reach review."
Description

Configurable rules engine that evaluates user inputs against community policies at runtime, detecting off-policy selections (e.g., prohibited colors, height limits, setback requirements) and dimension thresholds. Support rule definitions per community and per application type, with operators (>, <, in list, regex), and conditional logic. Provide clear violation messages with references to policy sections and suggest compliant alternatives. Integrate with validation pipeline and admin config. Expected outcome: policy compliance before submission and fewer manual reviews.

Acceptance Criteria
Scoped Rule Evaluation at Runtime
Given a community "Maple Oaks" with application type "Paint Approval" has three active rules And a user in "Maple Oaks" is editing a "Paint Approval" application When the user enters values for fields targeted by the rules Then only rules scoped to "Maple Oaks" and "Paint Approval" are evaluated And rules from other communities or application types are not evaluated And each evaluated rule returns a pass or fail result with ruleId, field targets, and computed operands
Operator Enforcement for Numeric, List, and Regex Fields
Given numeric rule "height < 8 ft" is active And numeric rule "setback > 5 ft" is active And list rule "color in [Cedar, Walnut]" is active And regex rule "lotId matches ^[A-Z]{2}[0-9]{3}$" is active When the user enters height = 8.1 ft, setback = 5 ft, color = "Red", lotId = "ab123" Then the engine returns four violations, one per rule And the numeric violations include actualValue and threshold in the payload And the list violation includes actualValue and allowedValues in the payload And the regex violation includes actualValue and pattern in the payload
Conditional Logic Application
Given conditional rule "IF zone = Corner AND fenceType = Solid THEN height <= 6 ft" exists And conditional rule "IF material IN [Metal, Vinyl] THEN color IN [Black, White]" exists When inputs are zone = Corner, fenceType = Solid, height = 6.5 ft, material = Metal, color = Bronze Then two violations are returned with conditionEvaluated = true When inputs are zone = Interior, fenceType = Solid, height = 6.5 ft Then no violation is created from the first rule because its condition is not met
Violation Messaging and Alternatives
Given a rule fails with metadata message = "Fence exceeds maximum height" And policySection = "Section 4.2.1" And alternatives = ["Reduce height to 6 ft", "Use permitted colors: Black, White"] When the engine returns the violation Then the violation payload includes message, policySection, and at least one alternative suggestion And the message is attached to the affected field(s) and includes the ruleId
Validation Pipeline Integration and Submission Control
Given an applicant updates a field on the application form When the field value changes Then the rules engine evaluates impacted rules and returns results within 500 ms at the 95th percentile And any failures are displayed inline on the affected fields and summarized in Readiness Check And the Submit action remains disabled while any off-policy violations exist And clearing or correcting values removes the flags within 500 ms at the 95th percentile
Admin Rule Configuration and Activation
Given an admin opens Rules Configuration for community "Maple Oaks" and application type "Fence" When the admin creates a new rule with operator, scope, condition, message, policySection, and alternatives Then the rule validates on save and persists successfully And the new rule becomes active for evaluations within 60 seconds of save When the admin edits or disables an existing rule Then subsequent evaluations reflect the change within 60 seconds
Attachment & Media Requirements Enforcement
"As an applicant, I want clear prompts for the photos and documents I must include so that I can submit everything correctly the first time."
Description

Enforce attachment requirements including minimum photo count, specific views (front, side), accepted file types, minimum resolution, and required documents (e.g., site plan). Provide guided capture on mobile with camera access, filename prompts, and automatic compression while preserving resolution thresholds. Detect duplicates and unreadable files. Display per-item checkmarks as requirements are satisfied. Expected outcome: complete, usable media at first submission and decreased follow-up.

Acceptance Criteria
Minimum Photo Count and Required Views Before Submission
Given the policy sets min_photo_count = 3 and required_views = [Front, Side] And the applicant has attached fewer than 3 photos or is missing any required view When the applicant attempts to submit the application Then submission is blocked And inline validation lists the missing count and the specific missing views And the Photos requirement displays a red state until both count and required views are satisfied When the applicant attaches photos and tags them to satisfy count and all required views Then the error message disappears And the Photos requirement shows a green checkmark
Accepted File Types and Minimum Image Resolution Validation
Given accepted_file_types = [JPG, JPEG, PNG, HEIC, PDF] and min_image_resolution = 1600x1200 px (policy) When a user uploads a file with an extension or MIME type not in accepted_file_types Then the file is rejected with an inline message listing allowed types When a user uploads an image below min_image_resolution (no upscaling) Then the file is rejected with an inline message stating the minimum resolution required and the detected resolution And the invalid file is not added to the attachment list
Required Document Presence (Site Plan) Enforcement
Given a required_document "Site Plan" with required_type = PDF When the user has not attached a file satisfying the "Site Plan" requirement Then the "Site Plan" requirement shows a blocking error and a CTA to "Upload Site Plan (PDF)" When the user attaches a PDF and designates it as "Site Plan" Then the requirement state switches to green checkmark And submission is allowed to proceed if no other blocking items remain
Mobile Guided Capture, Filename Prompts, and Smart Compression
Given the user is on mobile and taps "Add Photo" When camera permission is available Then a guided capture flow opens with step prompts for required views (e.g., Front, Side) And after capture, a filename prompt appears prefilled using the template [Address]_[View]_[YYYY-MM-DD] And the file name is validated against allowed characters and max length (<= 100 chars) When the captured image exceeds max_upload_size_mb but meets min_image_resolution Then the client compresses the image to <= max_upload_size_mb without reducing below min_image_resolution And if compression cannot meet both size and resolution constraints, the user is prompted to retake a higher-resolution photo When camera permission is denied Then the user is shown a permission prompt and a fallback to choose from library And EXIF orientation is preserved and images are auto-rotated to upright on upload
Duplicate Attachment Detection and User Resolution
Given an image is already attached When the user attempts to add another image with identical file hash or perceptual similarity >= 90% to an existing image Then the system flags it as a potential duplicate before finalizing upload And the user is prompted to Keep Both, Replace Existing, or Remove Duplicate (default: Remove Duplicate) And if Remove Duplicate is chosen, only one copy remains in the attachments list
Corrupt or Unreadable File Handling
Given a user uploads a file that is corrupt, zero-byte, password-protected, or otherwise unreadable When the system attempts to process the file Then the upload is rejected with a specific error reason (e.g., "File is corrupt" or "Password-protected PDF not supported") And the file is not added to the attachments list And the user is offered a Retry Upload CTA
Real-time Per-item Checkmarks and Submission Gating
Given the requirements panel lists each attachment rule as an item (Photos Count, Required Views, File Types, Resolution, Site Plan) When the user satisfies an item Then a green checkmark appears on that item within 1 second And a readiness percentage updates accordingly And the Submit button remains disabled until all mandatory items have green checkmarks When all mandatory items are satisfied Then the Submit button enables And the readiness status changes to "Ready to Submit"
Geospatial Trace Validation
"As a homeowner adding a fence line, I want the map to tell me if my trace crosses a setback so that I can adjust it before submitting."
Description

Validate that required map traces or placement markers are present, within the correct parcel boundaries, and meet policy constraints (e.g., distance from lot lines, easements). Support snapping to parcel polygons, measurement of lengths/areas, and unit selection. Provide real-time error/warning overlays and suggestions to adjust traces to compliant positions. Integrate with existing mapping component and parcel GIS data. Expected outcome: accurate site context and reduced rework due to invalid placement.

Acceptance Criteria
Required Geometry Presence and Parcel Containment
Given parcel polygons (including holes) for the application address are loaded And the policy requires at least 1 placement marker and 1 trace When the user attempts to submit with any required geometry missing Then submission is blocked with an error banner listing each missing geometry type And the canvas displays a "Required" callout at the corresponding tool When any vertex or segment of a geometry lies outside the parcel union or inside a parcel hole Then the offending geometry is outlined in red and a per-item error is listed And the error overlay includes the nearest distance to a valid interior in the currently selected units
Minimum Buffer Distance From Lot Lines and Easements
Given active policy constraints: minDistanceFromLotLine = 5 ft and minDistanceFromEasement = 3 ft When a marker or trace is positioned such that any point is closer than the configured minimum to a lot line or mapped easement Then a blocking error is shown on that geometry with the measured shortest distance And the submission action remains disabled until all distances are >= the configured thresholds When the user drags the geometry to a compliant position Then the error clears within 300 ms of pointer release and the status updates to Valid
Snapping to Parcel Edges and Compliance Guides
Given snapping is enabled and set to a 0.5 ft tolerance (0.15 m) When the user drags a vertex within the tolerance of a parcel edge or a compliance guide offset at 5 ft from the edge Then the vertex snaps to the target line and a snap indicator is displayed And the final distance to the target line is <= 0.25 ft (0.08 m) When snapping is toggled off Then no snapping occurs under the same conditions
Length/Area Measurement Accuracy and Unit Selection
Given unit selection options Feet and Meters are available When the user selects Feet Then all length labels display in feet with one decimal place and areas in square feet with one decimal place And switching to Meters converts the same values within 0.5% accuracy and updates labels within 200 ms When the user draws a 20 ft by 10 ft rectangle using the trace tool Then the displayed area is between 196.0 and 204.0 sq ft and perimeter is between 59.8 and 60.2 ft And the last chosen unit persists for the user when returning to the draft
Real-time Overlays and Auto-Correction Suggestions
Given a geometry violates the 5 ft lot-line buffer Then a red error overlay appears within 150 ms with the message "Move inward 5 ft" and a Fix action When the user clicks Fix Then the system computes the minimal inward offset to satisfy all active constraints and moves the geometry accordingly without crossing parcel holes or boundaries And the geometry becomes Valid and the overlay disappears And an Undo action restores the prior position
Validation on Submit, Summary Navigation, and Performance
Given parcel GIS layers are loaded in the existing mapping component When the user clicks Submit Then full geospatial validation executes in <= 800 ms for up to 2,000 vertices across all geometries And if violations exist, submission is blocked and a summary lists each issue with count and type And clicking a summary item pans/zooms to the offending geometry and selects it for edit When parcel GIS fails to load Then trace tools are disabled and a blocking message "Parcel boundary unavailable" is shown until data loads or the user retries
Contextual Inline Guidance & Auto‑Fix Suggestions
"As an applicant, I want helpful tips and quick fixes where I make mistakes so that I can correct them without leaving the form."
Description

Provide concise, inline tips adjacent to invalid fields with examples and microcopy, plus one-click auto-fixes where safe (e.g., auto-format dimensions, snap to nearest allowed height, convert units). Include learn-more links to policy excerpts. Ensure accessibility and localization of messages. Expected outcome: faster self-correction and lower abandonment.

Acceptance Criteria
Inline Tip Adjacent to Invalid Field with Example and Policy Link
- Given a field value violates a validation rule, When the user blurs the field or pauses typing for 500ms, Then an inline tip appears within 16px of the field showing: a concise message (≤140 chars), a valid example, and a Learn More link to the relevant policy excerpt. - Given the field value becomes valid, When validation passes, Then the inline tip hides within 200ms and no error state remains. - Given the user clicks Learn More, When the link is activated via mouse or keyboard, Then the policy excerpt opens in a non-blocking side panel or modal within 300ms and focus is managed to the panel header. - Given analytics is enabled, When the tip is shown, Then an event guidance_shown is logged with field_id and validation_rule_id.
One-Click Auto-Format of Dimensions and Unit Conversion
- Given the user enters a dimension in a non-canonical but parsable format or unit (e.g., 8 ft 6 in, 2.6m), When validation detects safe normalization, Then an Auto-fix action appears adjacent to the field. - Given Auto-fix is available, When the user clicks Auto-fix, Then the value is reformatted to the canonical display (e.g., 8'6") and converted to the policy-default unit with precision such that error ≤0.5%. - Given the value is auto-fixed, When the transformation completes, Then a confirmation toast appears within 300ms showing the new value and an Undo action available for 10s. - Given the normalization would violate min/max policy bounds or exceed tolerance, When validation runs, Then Auto-fix is not offered and the tip explains the required range. - Given analytics is enabled, When Auto-fix is applied, Then an event guidance_autofix_applied is logged with before and after values.
Snap to Nearest Allowed Value with Confirmation and Undo
- Given a numeric field has allowed discrete values or step increments, When the entered value is within the configured snapping tolerance (≤2% or ≤1 unit, whichever is smaller), Then a suggestion appears to snap to the nearest allowed value. - Given the user accepts the snap, When the action is confirmed, Then the field updates to the nearest allowed value and the validation state becomes valid. - Given snapping would cross a policy constraint (min/max) or change category, When computing the suggestion, Then no snap suggestion is shown. - Given a snap is applied, When the user clicks Undo within 10s, Then the original value is restored and the suggestion state resets.
Real-Time Missing Asset Guidance for Photos and Map Traces
- Given submission requires at least N photos and a closed map trace, When the user attempts to submit with missing photos or an open trace, Then submission is blocked and inline tips appear listing the specific missing items with actionable CTAs (Upload photo, Close trace). - Given a map trace has endpoints within 10px, When the user selects Close trace (Auto-fix), Then the system connects endpoints to close the shape and marks the trace valid. - Given a photo slot is missing, When the tip is shown, Then Upload and Drag-and-drop are both available and keyboard accessible, and selecting a photo removes the tip upon successful upload. - Given all missing items are resolved, When validation re-checks, Then the block is lifted and submission proceeds without page reload.
Off-Policy Selection Auto-Correct Suggestion Prior to Submission
- Given a dropdown or option selection is off-policy but has a nearest compliant alternative, When the user makes the off-policy selection, Then an inline warning appears with a one-click action to switch to the compliant option. - Given the user declines the suggestion, When they proceed to submit, Then submission is blocked with a clear explanation and a Learn More link to the specific policy excerpt. - Given the user accepts the suggestion, When applied, Then the field updates to the compliant value, the warning clears, and validation passes for that field. - The system never changes a user selection without explicit confirmation, and all changes are reversible via Undo within 10s.
Accessibility of Guidance Messages and Controls
- All guidance tips, links, and buttons meet WCAG 2.2 AA: color contrast ≥4.5:1, visible focus indicators, and full keyboard operability (Tab/Shift+Tab/Enter/Escape). - Tips are announced by screen readers via an ARIA live region (polite) within 500ms of appearance using meaningful, non-duplicative text. - Icons used in tips have accessible names/tooltips; no information is conveyed by color alone. - Appearance or dismissal of tips does not steal focus; pressing Escape dismisses the tip while preserving focus on the associated field.
Localization of Guidance Messages and Units
- Given the user’s language is supported (at minimum en-US and es-ES), When a tip is displayed, Then all message text and microcopy appear in the selected language; if unsupported, content falls back to English without mixed-language fragments. - Given a locale format, When numbers and units are shown in tips, Then they follow locale rules (e.g., decimal separator, thousands separator) and convert units to the community’s policy-default system. - Given a Learn More link is clicked, When localized policy excerpts exist, Then the corresponding localized excerpt opens; otherwise, the English version opens with a notice of fallback. - All guidance strings have localization keys; missing keys in non-production environments surface a visible placeholder and log an error.
Readiness Score & Submission Gate
"As a user, I want to see how close my application is to being ready so that I know what to fix before I submit."
Description

Display a dynamic readiness indicator (e.g., percentage or checklist) that aggregates validation results into blockers and warnings. Prevent submission until all blockers are resolved, while allowing submission with warnings if the community allows. Provide a pre-submit review screen summarizing outstanding items and suggested fixes. Expected outcome: higher-quality submissions and predictable review throughput.

Acceptance Criteria
Dynamic Readiness Indicator Aggregates Validations
Given an applicant is editing a submission with required fields (photos, map trace, dimensions) and policy checks (off-policy selections) When validations run in real time Then the readiness indicator displays a percentage calculated as (required validations passed / total required validations) × 100 rounded to the nearest whole number And the indicator shows counts for Blockers (failed required validations) and Warnings (non-blocking violations) And each blocker/warning is listed with a human-readable label and severity tag And each list item includes a Fix action that navigates to the related field
Submission Gating Based on Blockers and Policy
Given one or more blockers are present Then the Submit action is disabled and a helper message indicates blockers must be resolved And attempting to trigger submission via keyboard or API is prevented on the client And the server rejects any submission with blockers present Given only warnings remain and the community setting "Allow submit with warnings" is enabled Then the Submit action is enabled And clicking Submit proceeds to the pre-submit review with a warnings summary Given only warnings remain and the community setting "Allow submit with warnings" is disabled Then the Submit action is disabled with a message that warnings must be resolved per policy Given blockers are resolved When the final fix is applied Then the Submit action transitions to enabled within 300 ms
Pre-Submit Review Summarizes Outstanding Items
Given the applicant clicks Submit When blockers are zero and warnings ≥ 0 Then a pre-submit review screen is displayed And it shows readiness percentage, count of blockers (0) and count of warnings And it lists each outstanding warning with label, impacted field, and suggested fix text And each list item has a "Go to field" action returning to the corresponding form control And the review shows two actions: "Submit anyway" (if policy allows warnings) and "Cancel & Fix" And closing the review returns focus to the Submit control
Community-Configurable Severities and Policy
Given a community admin updates a validation rule severity (e.g., off-policy selection from Blocker to Warning) in settings When the applicant’s draft is opened or the rules are refreshed Then the readiness indicator reclassifies affected items and updates counts and percentage accordingly Given a community admin toggles the setting "Allow submit with warnings" When an applicant views a draft Then the gating behavior reflects the current setting without requiring a page reload And the pre-submit review shows actions consistent with the policy Given settings are saved Then changes persist across sessions and are applied uniformly to all applicants in that community
Real-Time Updates and Inline Fix Navigation
Given the applicant corrects a field that previously caused a blocker (e.g., adds required photos or completes a map trace) When the field change is committed Then the readiness percentage and blocker/warning counts update within 300 ms And the resolved item is removed from the list without a full page reload Given the applicant clicks a Fix action for an item Then the view scrolls (or navigates) to the target field and programmatically focuses the first interactive control And an inline tip is displayed adjacent to the field describing how to resolve the issue
Submission Captures Readiness Snapshot and Server Enforcement
Given the applicant submits a form Then the submission payload contains readinessSnapshot with fields: percentage (0–100), blockers (array, length 0 at submit), warnings (array), and timestamp And the server validates that blockers length is 0 before accepting the submission And if blockers are detected server-side, it responds with HTTP 409 and error code "BLOCKERS_PRESENT" And the client displays the pre-submit review highlighting the server-reported blockers
Accessible Indicator and Controls
Given the readiness indicator changes state (e.g., blockers resolved) Then a live region announces the updated readiness and Submit availability for screen readers And the indicator conveys state via text and icons, not color alone, meeting WCAG 2.1 AA contrast requirements Given the pre-submit review is opened Then focus moves to the review header, tab order is trapped within the dialog, and ESC or the close control dismisses it And on dismiss, focus returns to the originating Submit control
Admin Policy Configuration & Reporting
"As a community admin, I want to configure validation rules and requirements without code so that our policies are enforced consistently across submissions."
Description

Provide an admin UI for boards/managers to define required fields, conditional logic, media requirements, and policy rules per application template and community. Support versioning, effective dates, sandbox testing, and rollback. Include basic reporting on validation outcomes (common failures, time to fix) to inform policy tuning. Integrate with role-based access control and audit trail. Expected outcome: self-serve policy maintenance and continuous improvement of approval rates.

Acceptance Criteria
Policy Version Creation and Effective Date Activation
Given I am an authorized Policy Editor for Community C and Template T When I create a new policy version V with a future effective start datetime and save it Then the version is stored as Scheduled with a unique, incremented version identifier And only one version per Community C and Template T is Active at any point in time And at the effective start datetime, V transitions to Active automatically without manual intervention And Readiness Check validations for submissions initiated after activation use V within 60 seconds of activation And submissions initiated before activation continue to validate against the previously active version
Conditional Logic and Field Requirements per Template and Community
Given a rule is defined on Template T in Community C: If Project Type equals "Fence", require fields Height and Material When an applicant selects Template T in Community C and sets Project Type to "Fence" Then Height and Material are marked as required and missing values trigger readiness flags with admin-defined hint text And if Project Type is not "Fence", Height and Material are not required by this rule And rules support AND/OR conditions and operators (=, !=, in list, >, <) and can target specific communities and templates And rules not scoped to the applicant's community or template are not applied
Media Requirements Configuration and Enforcement
Given an admin configures media requirements for Template T in Community C: minimum 3 photos, at least one map trace, and dimensions in feet with min value 1 and max 100 When an applicant attempts to submit with fewer than 3 photos, no map trace, or out-of-range dimensions Then the Readiness Check displays specific, actionable errors for each unmet requirement and prevents submission until resolved And only image formats jpg and png up to 10 MB each are accepted if configured, with invalid files rejected with error messaging And admin-defined inline tips are displayed next to each flagged field or media slot And any configured disallowed option selection is flagged before submission
Sandbox Test Workflow and Publish
Given I create a draft policy version V for Template T in Community C When I open Sandbox mode and run a test case using sample application data against V Then the system executes Readiness Check using V only in Sandbox context and displays pass/fail per rule with timestamps And Sandbox results are logged and exportable as CSV And Sandbox mode has no effect on live applicants or Active policy behavior And when I publish V, it becomes Scheduled or Active based on its effective date and is visible in version history
Rollback to Prior Version with Audit Trail
Given Version V2 is Active for Template T in Community C and Version V1 exists in history When I initiate a rollback to V1 and provide a mandatory reason Then V1 becomes Active within 60 seconds and V2 moves to Superseded state without deletion And all actions (who, what, when, reason, diffs) are captured in the audit trail and are immutable and filterable by user, template, community, and date range And Readiness Check immediately uses V1 for new validations after rollback
RBAC Enforcement for Policy Administration
Given system roles exist: Owner, Manager, Policy Editor, Viewer When a Viewer or unauthorized user attempts to create, edit, publish, schedule, or roll back a policy Then access is denied with a clear message and no changes are saved And Policy Editors can create and edit drafts and run Sandbox tests but cannot publish or roll back And Owners and Managers can publish, schedule, and roll back; all actions respect community scoping And all permission checks are logged in the audit trail
Validation Outcomes Reporting and Insights
Given validation events are captured for Readiness Check across communities and templates When an admin views the Reporting dashboard for a selectable date range Then they can see the top failed rules, counts, percentage of impacted submissions, median and average time-to-fix, and approval rate trend compared to the prior period And reports are filterable by community, template, policy version, and rule And metrics update within 15 minutes of new events and can be exported to CSV And personally identifiable applicant data is redacted from reports

Review Pack

One click compiles a clean, exportable packet: cover summary, captioned photos, plot overlay, specs, and signatures. Optional redaction protects personal details for neighbor sharing while preserving a full audit copy for the committee.

Requirements

One-Click Pack Generation from Feed
"As a board member, I want to create a complete, standardized packet from a post with one click so that I can quickly prepare professional documentation without manual assembly."
Description

Generate a complete Review Pack with a single action from any eligible feed item (e.g., compliance case, architectural request). Auto-assemble a clean packet containing a cover summary, item metadata (address/lot, case ID, dates), selected attachments, captioned photos, plot overlay, specifications, and collected signatures. Use a default, community-scoped template with configurable sections and ordering. Ensure deterministic rendering, consistent styling, page numbering, and a contents page. Validate required fields and gracefully prompt for missing data before generation. Store the generated pack with immutable versioning and link it back to the originating feed item.

Acceptance Criteria
One-Click Generation from Eligible Feed Item
Given an eligible feed item type (Compliance Case or Architectural Request) is selected and the user has "Generate Review Pack" permission When the user clicks "Generate Review Pack" from the feed item action menu Then generation starts without navigating away and a progress indicator appears within 300 ms And the action is disabled with a tooltip "Not available for this item" for ineligible item types And for a standard dataset (≤25 pages, ≤20 attachments totaling ≤50 MB), the pack is generated within 10 seconds at the 95th percentile
Preflight Validation for Required Fields
Given one or more required fields (address/lot, case ID, relevant dates, at least one contact) are missing from the feed item When the user initiates pack generation Then a preflight checklist modal enumerates each missing field with inline editors or deep links to edit the item And the Generate action remains disabled until all required fields are supplied And if the user cancels, no pack is created and no version is stored And once all required fields are present, generation proceeds successfully
Pack Contents and Ordering Per Community Template
Given a community-scoped default template with a configured section order exists When a pack is generated for an eligible item Then the output contains sections per template in the configured order: Cover Summary; Item Metadata (address/lot, case ID, dates); Selected Attachments; Captioned Photos; Plot Overlay; Specifications; Signatures; Table of Contents And sections without data are omitted unless configured as "Show when empty" And a Table of Contents is included whose entries map to correct page numbers And page numbering appears as "Page X of Y" in the footer across all pages starting at the cover And selected attachments are included and non-selected attachments are excluded; photo captions render beneath corresponding photos And typography, colors, and spacing match the community’s Review Pack style tokens
Deterministic Rendering Across Re-Generations
Given the same feed item data, same template version, same attachment/photo selections, and same renderer version When the pack is generated multiple times Then the resulting PDF files are byte-identical (matching SHA-256 hash) And timestamps and other volatile metadata are normalized to ensure deterministic output And any change to item data, selections, or template version results in a different hash
Immutable Storage, Versioning, and Linkage
Given a pack version already exists for a feed item When a new pack is generated after any change Then a new immutable version is stored with version incremented by 1 And prior versions remain accessible read-only and cannot be altered or deleted by end users And each stored version records Pack ID, Version, Template Version, Created At (UTC), Created By (user ID), SHA-256 hash, and Page Count And the originating feed item displays a link to the latest pack and a version history list And the latest alias URL remains constant while version-specific URLs are unique and stable
Attachment Selection and Optional Redaction
Given the user has reviewed attachments and selected those to include When the pack is generated Then only the selected attachments are included, honoring the template/selection order And photo attachments display their saved captions beneath each image And if the community setting "Enable redacted shareable pack" is ON, the user can opt to generate both an Audit Copy and a Redacted Copy And the Redacted Copy masks configured PII (emails, phone numbers, account numbers, signatures) while preserving layout and pagination And both copies are stored, distinctly labeled, and share the same version number with copy-type tags
Permissions and Audit Logging
Given a user without the "Generate Review Pack" permission views an eligible feed item Then the "Generate Review Pack" action is hidden in the UI and API attempts return HTTP 403 with error code RP_FORBIDDEN Given a user with permission generates a pack Then an audit event is recorded with user ID, feed item ID, template version, pack version, UTC timestamp, and outcome (success/failure with error code) And audit logs are immutable and retrievable by authorized administrators
Redaction Profiles & Dual Output
"As a compliance chair, I want to automatically generate a redacted neighbor copy and a full audit copy so that we can share transparently while protecting residents’ personal information and preserving an internal record."
Description

Apply configurable redaction profiles to produce two outputs from the same source: a neighbor-share copy with personal data masked (e.g., owner names, emails, phone numbers, account/lot identifiers, signatures) and a full audit copy for committee/internal records. Support field-level rules, region-based redaction on images, and selective exclusion of sections. Visually watermark the redacted copy (e.g., “Neighbor Copy”) and include a redaction log page summarizing what was masked. Maintain an unaltered, access-restricted audit copy with hash-based integrity verification.

Acceptance Criteria
Create and Apply Redaction Profile with Field-Level Rules
- Given I have Board or Manager permissions in a community, When I create a redaction profile named uniquely within that community, Then I can enable field-level rules for owner name, email, phone, account ID, lot identifier, and signatures. - When I save the profile, Then the system validates that at least one rule is enabled and the profile name is unique; otherwise, save is blocked with inline errors. - Given a Review Pack source, When I apply the saved profile, Then all configured fields in text and metadata are masked according to each rule’s masking format (e.g., replace with "REDACTED" or pattern "••••"). - Then the export metadata records the profile name and version used.
Region-Based Image Redaction
- Given a Review Pack page containing images, When I select "Add image redaction," Then I can draw one or more rectangles or polygons to define redaction regions and assign them to the active profile. - When the neighbor copy is exported, Then pixels within each defined region are irreversibly obscured (e.g., solid fill or blur) and cannot be programmatically recovered. - Then the audit copy renders the same images without any obscuration. - Then the redaction log lists each region with page number and coordinates without revealing underlying content.
Dual Output Generation from Single Source
- Given a prepared Review Pack and a selected redaction profile, When I click "Export," Then two files are generated in one operation: a Neighbor Copy (redacted) and an Audit Copy (full), in the chosen format (e.g., PDF). - Then both outputs contain identical content structure and ordering except for applied redactions, the watermark, and the presence of a redaction log page only in the Neighbor Copy. - Then the export completes successfully only when both files are available with distinct filenames that include the profile name and timestamp. - Then the system associates both outputs to the same export job ID for traceability.
Watermark and Redaction Log on Neighbor Copy
- Given a redacted Neighbor Copy, When I view any page, Then a visible "Neighbor Copy" watermark appears on every page at 20–40% opacity and does not obscure primary content (text remains legible at WCAG AA contrast). - Then the Neighbor Copy includes a Redaction Log page summarizing: profile name and version, list of rules applied (type), count of items masked per rule, and page/section references for each. - Then the Redaction Log contains no original redacted values or images. - Then removal or editing of the watermark/log within the app is not permitted for that export artifact.
Integrity-Protected Audit Copy with Access Controls
- Given the Audit Copy is generated, When the system computes its SHA-256 hash at export time, Then the hash is stored with the export metadata and displayed in the export details. - When a user downloads the Audit Copy later, Then the system recomputes the hash and displays a match/verify result to the user before download begins. - When a non-committee/non-manager attempts to access the Audit Copy, Then access is denied (HTTP 403-equivalent) and the attempt is logged. - Then the Audit Copy contains no watermark or redaction log and matches the unredacted source content used at export (byte-size and page count parity).
Selective Section Exclusion in Neighbor Copy
- Given a redaction profile with section-exclusion rules, When I export the Neighbor Copy, Then specified sections (e.g., payment history, signature pages, contact sheet) are omitted entirely from the Neighbor Copy. - Then page numbering and any table of contents/bookmarks are updated to reflect removed sections. - Then the Audit Copy includes all original sections regardless of exclusion rules. - Then the Redaction Log notes which sections were excluded by name/ID and page range.
Performance, Resilience, and Audit Trail
- Given a 20-page Review Pack with 10 images and active field and region redactions, When I generate both outputs, Then server-side processing completes within 10 seconds at P95 under nominal load and within 25 seconds at P99. - When any redaction or export step fails, Then neither output is published, the user sees a clear error with remediation steps, and a retry action is available without data loss. - Then all export events are logged: user, timestamp, profile name and version, rule counts, hash of Audit Copy, output filenames, duration, and outcome (success/failure).
Parcel/Plot Overlay Renderer
"As an architectural committee member, I want a precise plot overlay in the packet so that reviewers can understand location context at a glance without opening separate mapping tools."
Description

Embed a clear plot overlay that highlights the relevant lot, boundaries, and incident/installation location using stored parcel data or uploaded/community maps. Support coordinate anchoring, scale bars, legend, north arrow, and labeled callouts. Allow pin/shape annotations and color themes aligned with community branding. Cache overlays per case to avoid regeneration, and include alt text for accessibility. Ensure printable resolution and accurate geospatial alignment where source data supports it.

Acceptance Criteria
Render Overlay from Stored Parcel Data
Given a case with a linked parcel ID and boundary geometry in EPSG:4326 or EPSG:3857, When the Review Pack is generated, Then the overlay highlights the target parcel with a 2 px border and 40% fill opacity and appears in on-screen preview and export. Given lot/block/address attributes exist on the parcel, When the overlay renders, Then those attributes are available as label options and appear if toggled on in the legend and map callouts. Given the parcel geometry is missing or invalid, When generation runs, Then the pack renders without the overlay, logs an error with parcel ID, and displays an 'Overlay unavailable' notice in the Review Pack summary.
Render Overlay from Uploaded Community Map with Anchor Control Points
Given a community raster map (PNG/JPG/PDF) is uploaded and ≥3 non-collinear control points are set with coordinates, When anchors are saved, Then the system georeferences the map with mean residual error ≤ 2 px at 100% display scale and stores the transform per community. Given a georeferenced map and a case location point, When the overlay renders, Then the parcel boundary and case point align with the basemap within ≤ 3 m at zoom level 19 where basemap accuracy supports it. Given <3 valid control points or inconsistent coordinate pairs, When the pack is generated, Then the overlay from the uploaded map is skipped and a validation message lists required corrections.
Scale Bar, Legend, North Arrow, and Print Quality
Given the overlay is displayed at any zoom level, When the user zooms in or out, Then the scale bar updates to correct metric and imperial values within ±2% of basemap scale. Given at least one overlay layer is visible, When exported to PDF, Then the legend lists visible layers, symbology swatches, stroke widths, and hex color codes. Given the map is rendered in any projection, When exported, Then the north arrow orientation deviates ≤ 5° from true north for that projection. Given the user selects Print Quality export, When the PDF is generated, Then overlay lines and text are vectorized where possible; otherwise raster tiles render at ≥ 300 DPI with minimum 1 pt line width, and the PDF page includes a scale bar and north arrow.
Annotation Tools: Pins, Shapes, and Labeled Callouts
Given a user adds a pin, polyline, or polygon annotation, When the annotation is saved, Then it persists to the case overlay cache and reappears on reload and in export. Given a labeled callout is created, When exported to PDF and image formats, Then the label text renders at ≥ 9 pt with no truncation at 100% and 200% zoom, and the leader line connects within 2 px of its target geometry. Given snapping is enabled, When drawing within 8 px of parcel vertices or edges, Then the cursor snaps with a visual indicator and the placed vertex lies within 5 px of the target.
Brand-Aligned Color Themes and Contrast
Given community branding defines primary and accent colors as hex values, When the overlay renders, Then parcel highlight, annotations, and legend use those colors, and text/background combinations meet WCAG 2.1 contrast ratio ≥ 4.5:1 (normal text) or ≥ 3:1 (large text). Given branding colors are not configured, When rendering, Then a default theme is applied and recorded in the export metadata.
Case-Level Overlay Caching and Invalidation
Given a case overlay has been generated, When the same overlay is requested with no changes to parcel geometry, anchors, theme, or annotations, Then the cached version is served with server processing time ≤ 300 ms and a cache hit is logged. Given any source data or annotation changes, When the overlay is requested, Then the cache invalidates and a new overlay is generated with server processing time ≤ 2 s for 1920x1080 preview and ≤ 5 s for A4/Letter 300 DPI export; the new cache version ID and timestamp are stored. Given cache storage exceeds 30 days age or 200 MB per case, When a new overlay is generated, Then least-recently-used cached versions are purged and the action is logged.
Accessibility: Alt Text, Keyboard, and Screen Reader Semantics
Given the overlay image or map is included in HTML and PDF outputs, When exported, Then each image has alt text summarizing lot identifier, scale, and key annotations in ≤ 150 characters, programmatically associated with the image element. Given keyboard-only navigation, When focusing the overlay canvas and tools, Then all annotation functions are operable via keyboard with visible focus indicators, no keyboard trap, and shortcuts are discoverable via a tooltip or help link. Given a screen reader is active, When the legend is present, Then it is exposed as a labeled list structure and color meanings are described in text (not color alone), enabling non-visual interpretation.
Export Formats & E‑Signature Inclusion
"As a manager, I want to export a polished PDF (and ZIP of originals) that includes verified signatures so that I can distribute and archive legally sound packets."
Description

Export the Review Pack as a paginated PDF suitable for print and archival, and optionally a ZIP containing original attachments. Include captured e-signatures and an appended certificate page with signer identities, timestamps, and signature verification metadata. Preserve document outline/bookmarks for sections, embed fonts, and optimize file size for email. Support page-level QR/link back to the source record in Duesly. Validate signature inclusion order and authenticity indicators where applicable.

Acceptance Criteria
Export Review Pack to Paginated PDF
Given a Review Pack containing a cover summary, captioned photos, a plot overlay, specs, and signatures When the user exports the Review Pack as PDF Then the system generates a single paginated PDF with page numbers and consistent margins of at least 0.5 inches And each section begins on a new page in the order: Cover Summary, Photos, Plot Overlay, Specs, Signatures, Certificate And the PDF opens in Adobe Acrobat Reader and Apple Preview without repair or font substitution warnings And all text is selectable and searchable And images render at a minimum effective resolution of 150 DPI at printed size
Include E‑Signatures and Certificate Page
Given a Review Pack with captured e‑signatures from one or more signers When the user exports as PDF Then e‑signature representations are included on the appropriate pages exactly where they appear in the Review Pack And an appended certificate page is added as the last section And the certificate lists each signer’s full name, email, and, when captured, IP address and device information And each signer entry includes an ISO 8601 timestamp with timezone of signing and the signing sequence number And the certificate includes a document hash, signature method, and a verification URL referencing the Duesly record And the order of signers on the certificate matches the signing sequence And any signature authenticity indicators available in the source are preserved
Preserve Document Outline and Embed Fonts
Given a Review Pack with standard sections and photo captions When the user exports as PDF Then the PDF includes a document outline/bookmarks with top‑level entries for Cover Summary, Photos, Plot Overlay, Specs, Signatures, and Certificate And each photo has a child bookmark labeled with its caption And opening the PDF’s document properties shows all used fonts are embedded (subset or full) with no missing fonts And navigating via bookmarks jumps to the correct pages
Optimize PDF for Email Delivery
Given a Review Pack with up to 100 pages and up to 50 photos And the user selects Optimize for Email during export When the PDF is generated Then the resulting PDF file size is 20 MB or less And all text remains vector/selectable and not rasterized And images are downsampled and compressed to maintain at least 150 DPI at printed size without visible artifacts And e‑signature visuals and the certificate page remain intact and legible
Page‑level QR Code and Link Back to Duesly
Given an exported PDF Review Pack When the PDF is viewed or printed Then each page footer contains a QR code and a short HTTPS URL that resolve to the source Review Pack record in Duesly And scanning the QR code with a mobile device opens the correct Duesly record And following the link without sufficient permissions prompts authentication and does not expose protected content And the QR and link include a stable identifier sufficient to retrieve the exact exported pack version
Optional ZIP Export of Original Attachments
Given a Review Pack containing original attachments And the user selects Export as ZIP or Include Original Attachments When the ZIP export completes Then the ZIP contains the original files in their original formats with sanitized filenames And the ZIP includes a manifest.json listing each file’s original name, type, size, and SHA‑256 hash, and a pointer to the Duesly record And the ZIP includes the same PDF Review Pack (optimized for archival) and the certificate page And extracting the ZIP recreates the folder structure without errors and all hashes validate
Captioned Media Curation
"As an inspector, I want to curate and caption evidence photos so that reviewers can quickly understand what each image shows and why it matters."
Description

Provide an editor to select, order, and caption photos and other media for inclusion. Auto-extract timestamps, uploader, and location metadata when available, and allow inline annotations (arrows, callouts, blur). Enforce consistent caption style and numbering, and generate a media index page. Support basic image adjustments (crop, rotate, brightness) that are non-destructive to originals. Ensure all captions are included beneath images in the packet and in the accessibility text.

Acceptance Criteria
Select, Order, and Caption Media for the Packet
Given a review pack editor with a media library containing images and documents When the editor selects multiple items and reorders them via drag-and-drop Then the chosen set and order are persisted with the draft and restored on reopen Given an included media item When the editor adds a caption Then the system auto-prefixes it with an incrementing figure number (e.g., "Figure 1:") and enforces the configured caption style (typography and casing) Given missing captions on any included media When the editor attempts to export Then the export is blocked with a clear list of items requiring captions
Auto-Extract and Manage Media Metadata
Given an uploaded image with EXIF timestamp, GPS location, and recorded uploader When it is added to the editor Then the timestamp, location, and uploader fields auto-populate, with time normalized to the community’s configured timezone Given a media item lacking any of these metadata fields When it is added to the editor Then the corresponding fields remain empty without error and are editable Given auto-populated metadata When the editor overrides a value Then the override is stored for the packet without altering the original file’s metadata
Inline Annotations on Images
Given an included image When the editor uses arrow, callout, and blur tools Then annotations render as non-destructive overlays that persist with the draft and export Given a blur annotation applied to obscure personal details When the packet is exported Then the blur is applied in all export formats and cannot be reversed from the export artifact Given multiple annotations on one image When the editor reopens the draft Then each annotation remains editable (move, resize, delete) without quality loss to the base image
Non-Destructive Image Adjustments
Given an included image When the editor crops, rotates (in 90° increments), or adjusts brightness Then the preview updates immediately, adjustments are saved as reversible edits, and the original file remains unchanged Given images with adjustments When the packet is exported Then the adjusted appearance is used in the packet while the system retains the original file and an audit of adjustments
Caption Placement and Accessibility Text
Given any included media with a caption When the packet is exported Then the caption is rendered directly beneath the media with the enforced style and figure number Given a media item with a caption When the packet is exported Then the media’s accessibility alt text includes the full caption text and figure number without truncation Given a media item with multiline captions When exported Then the caption wraps under the media without overlap or loss of content
Media Index Page Generation
Given a packet with one or more included media items When the packet is exported Then an index page is generated listing each item’s figure number, caption, and page number, ordered by appearance, and inserted immediately after the cover Given the editor reorders media or edits captions When the packet is re-exported Then the index updates to reflect the new order and captions Given the export is a PDF When the index is viewed Then each index entry links to its corresponding page
Include Supported Non-Image Media
Given a supported non-image media item (e.g., PDF or video) When it is added to the editor Then it can be ordered and captioned like images, appears in the export with its caption beneath, and is listed in the media index with its figure number Given a non-image media item When editing tools are shown Then image-specific adjustments and annotations are disabled, while captioning and ordering remain available
Access Control, Sharing Links & Activity Log
"As a board secretary, I want controlled sharing with tracked access so that we can distribute the right copy to the right audience and retain a clear record of who saw what and when."
Description

Enable role-based access to generated packs with shareable, expiring links for neighbors, password protection, and optional download/print restrictions for the redacted copy. Log all access events (views, downloads, expiry) and surface them on the originating feed item. Respect Duesly’s existing permissions, ensuring only authorized roles can view audit copies. Provide revoke/extend controls and track which version was shared to maintain a defensible audit trail.

Acceptance Criteria
Share Redacted Review Pack via Expiring Link
Given a board member or manager with permission to share redacted copies When they generate a share link and set an expiration date/time (UTC) Then the system creates a unique, non-guessable URL token (>=128-bit entropy) that serves only the redacted version of the selected Review Pack Given the link is accessed before expiration When a neighbor opens it in a browser Then the redacted pack renders successfully without requiring login and the response does not expose audit-only fields Given the link reaches its expiration timestamp When it is accessed Then the system returns an expired state and no content bytes are served Given the link owner views link details on the originating feed item When they inspect it Then the UI shows created-by, created-at, expiry-at, and redacted-version ID
Password-Protect Shared Link
Given password protection is enabled during link creation When a password meeting policy (minimum 8 characters) is set Then the link requires the password before rendering content Given repeated incorrect password attempts occur When 5 consecutive failures from the same IP happen within 10 minutes Then the system enforces a 15-minute cooldown for that IP and logs the failures Given the link owner updates or removes the password When changes are saved Then subsequent accesses respect the new setting and the change is logged without storing the plaintext password Given an authenticated Duesly user accesses via the link When they enter the correct password Then the view is granted and associated to their user ID in the activity log; unauthenticated views associate to link ID and IP only
Restrict Download and Print on Redacted Copy
Given download/print restriction is toggled on for the share link When a neighbor opens the link Then the viewer hides download/print controls and streams content inline with no direct file URL exposed Given a direct file endpoint is requested for that share token When attempting to download the underlying file Then the system responds with access denied and logs the blocked download Given download/print is restricted When content is displayed Then a visible VIEW ONLY watermark with share-link ID and timestamp appears on each page
Enforce Role-Based Access for Audit Copies
Given Duesly’s permissions matrix When a user without an authorized role attempts to access an audit (unredacted) copy by any route Then access is denied and the attempt is logged Given a user with an authorized role opens the audit copy from the feed item When they view it Then the audit copy renders with full fields and is not available via neighbor share links Given a neighbor share link is created When it is used by any viewer Then it always serves the redacted copy only, regardless of authentication state
Activity Logging and Feed Surfacing
Given any share link lifecycle event occurs (create, view, download allowed, print attempt, password failure, revoke, extend, expire) When the event happens Then the system records: event type, timestamp (UTC), actor (user ID if authenticated; else share-link ID and IP), user agent, and redacted-version ID or audit-copy ID Given the originating feed item is opened by an authorized user When they view the Activity section Then they see a chronological list of events with filters by event type and date, plus summary counts (views, downloads, password failures) Given an authorized user exports the log When they click Export Then a CSV is generated containing all captured fields for the selected date range
Revoke and Extend Share Links
Given a share link exists When the owner clicks Revoke and confirms Then the link is immediately invalidated, all subsequent requests return expired state, and a revoke event is logged Given a share link exists When the owner extends the expiration to a future date/time Then the new expiry is persisted, reflected in the UI, and an extend event is logged Given a revoked link is extended When the owner sets a new future expiration Then the link becomes valid again and the reactivation is logged as an extend event
Version Tracking and Evidence Integrity
Given a share link is created When the redacted pack is generated Then the system assigns and stores an immutable version ID (hash) of the exact content served via that link Given the underlying Review Pack is edited after sharing When the original share link is accessed Then it continues to serve the originally shared version unless a new link is generated Given an authorized auditor opens the activity log on the feed item When they inspect an event Then they can see the associated version ID and verify its integrity by checksum comparison on export

Smart Auto‑Mask

Automatically detects and blurs faces, license plates, and house numbers the moment photos are captured or uploaded. Tunable blur strength and mask style (blur, pixelate, box) keep sensitive details private without extra steps—saving time and preventing accidental exposure before any sharing.

Requirements

Real-time PII Detection
"As a community manager, I want sensitive areas like faces, plates, and house numbers to be detected automatically on upload so that I avoid accidentally exposing residents’ personal information."
Description

Automatically detects faces, license plates, and house numbers at image capture and upload using optimized computer vision models, returning bounding boxes and confidence scores within strict latency targets. Supports common formats (JPEG, PNG, HEIC), varied lighting and angles, and multi-image posts. Runs on-device when available with server fallback to preserve responsiveness and privacy. Exposes detection results to the masking pipeline and logs metadata for audit and model tuning. Integrates with Duesly’s post composer and media upload flow so detections complete before any preview or sharing.

Acceptance Criteria
Pre-Preview Real-Time Detection SLA
Given a user is in the Duesly post composer and captures or uploads a single image When the image is handed to the Real-time PII Detection service Then bounding boxes with class labels (face, license_plate, house_number) and confidence scores are returned before any image preview is shown And on-device inference completes within 200 ms p95 and 500 ms p99 for images ≤ 12 MP And server fallback completes within 600 ms p95 and 1,200 ms p99 round-trip for images ≤ 12 MP on a 10 Mbps connection And if detection has not completed within 2,000 ms, the UI shows “Analyzing…” and prevents preview/share until results arrive or the user cancels
Format & Orientation Support
Given the user uploads images in JPEG, PNG, or HEIC up to 25 MB and 12 MP, with arbitrary EXIF orientation When detection runs Then decoding succeeds without user-visible errors And EXIF orientation is respected so that returned bounding boxes align with the visually rendered image within ±2 pixels after scaling And color profiles (sRGB, Display P3) are preserved in the preview; detection operates on a correctly converted RGB buffer And HEIC images are supported on iOS and Android; on web, HEIC is transcoded server-side without exceeding the SLA in this set
Detection Quality Thresholds Across PII Types
Given the default detection threshold of 0.5 confidence When evaluated on the internal validation set representing varied lighting (50–2,000 lux), occlusion (up to 40%), and oblique angles (up to 60° yaw/pitch) Then face detection achieves ≥ 0.95 recall and ≥ 0.90 precision And license plate detection achieves ≥ 0.92 recall and ≥ 0.88 precision And house number detection achieves ≥ 0.90 recall and ≥ 0.85 precision And no single class’ false negative rate exceeds 8% in the low-light subset
Multi-Image Post Completion Gate
Given a user selects a post with 2–10 images When detection runs concurrently across images Then detection results are produced for every image before any preview grid or sharing controls are enabled And per-image latency meets the SLA in this set; total wall-clock time for the batch is ≤ max(per-image p95) + 300 ms orchestration overhead And if any image fails detection, the post remains blocked from preview/share, and the failing image is clearly flagged with a retry action that succeeds ≥ 99% within two attempts
On-Device First with Secure Server Fallback
Given a device with a supported on-device model and sufficient resources When detection starts Then on-device inference is used and no image bytes leave the device; only metadata is logged And if on-device initialization exceeds 300 ms or inference exceeds the p95 SLA, server fallback is invoked automatically And server fallback transmits images over TLS 1.2+ and deletes image bytes within 60 seconds after processing; only non-content metadata is retained And server and on-device outputs are schema-identical and within ±3% IoU overlap on bounding boxes for the same image
Masking Pipeline Integration and API Contract
Given detection completes When results are consumed by the masking pipeline Then each detection includes: image_id, class, confidence [0–1], bbox in normalized [x, y, w, h] relative to post-rotation orientation And default masks are applied (style and strength from user preferences) to all detections before any preview is rendered And toggling mask style/strength in composer updates masks without re-running detection And coordinates map from original to display space with ≤ 2 px error at 3x zoom
Audit Logging and Observability
Given each detection run When logging occurs Then the system records timestamp, pseudonymous user/device identifier, model version, path (on-device/server), image dimensions, counts per PII type, confidence threshold, per-image inference time, and outcome (success/failure) And no raw image data or cropped PII regions are logged; only a salted SHA-256 hash of the image is stored for de-duplication And logs are queryable in the admin dashboard within 5 minutes of ingestion and retained for 90 days And p95/p99 latency and detection volume metrics are visible with daily aggregates
Instant Mask Application
"As a board member, I want masks applied instantly with a chosen style and strength so that I can share updates without manual editing or privacy risk."
Description

Applies masks to all detected regions before images are displayed, shared, or exported anywhere in Duesly, using configurable styles (blur, pixelate, box) and tunable strength. Uses a non-destructive pipeline that generates masked derivatives while preserving the original separately, ensuring masked versions appear in the feed, announcements, invoices, compliance posts, push/email previews, and exports by default. Handles per-image and per-object styling, supports batch processing for galleries, and annotates outputs with safe-to-share flags.

Acceptance Criteria
Pre-Display Instant Masking Across Surfaces
Given a user uploads or captures an image anywhere in Duesly And organization masking defaults are set When the Smart Auto‑Mask pipeline processes the image Then all detected faces, license plates, and house numbers are masked according to active settings before any render, share, or export occurs And no unmasked version is ever visible in the feed, announcements, invoices, compliance posts, galleries, or previews And a neutral placeholder is shown until masking completes (no unmasked frame displayed) And time to first masked render is <= 2 seconds p95 for a single image ≤ 12 MP on the reference environment
Configurable Styles and Tunable Strength (Per-Image and Per-Object)
Given org-level defaults specify mask style (blur, pixelate, box) and strength (0–100) When a new image is masked Then the default style and strength are applied Given a user sets per-image overrides for style and strength When the image is reprocessed Then all detected regions in that image use the per-image overrides Given a user sets per-object overrides (e.g., face=box 70, plate=pixelate 60, house=blur 80) When the image is reprocessed Then each region uses its per-object override And precedence is per-object override > per-image override > org default And values outside the allowed ranges are rejected with a validation error
Non-Destructive Originals with Masked Derivatives and Safe-to-Share Flags
Given an image is uploaded When processing completes Then the original image is stored unmodified and separately from masked derivatives And a masked derivative is created for display and distribution And the derivative carries metadata safe_to_share=true and references its source; the original is marked safe_to_share=false When any UI or export endpoint requests the image Then the masked derivative is returned by default and the original is never returned by those endpoints When remasking is requested (style/strength change) Then the new derivative is generated from the preserved original, not from an existing derivative
Batch Processing for Galleries
Given a user uploads a gallery of up to 50 images in a single action When processing begins Then each image is queued and masked independently And the gallery does not become visible to non-authors until all images have masked derivatives or are explicitly marked as failed And per-image and per-object overrides can be applied in batch and are respected for each image And processing throughput is ≥ 10 images per minute p90 for ≤ 12 MP images on the reference environment And any failed images are withheld from view/share/export and surfaced with a retry option
Fail-Safe On Masking Errors or Timeouts
Given the masking pipeline errors or exceeds a 10-second timeout for an image When the user attempts to view, share, or export that image Then no unmasked pixels are rendered, shared, or exported And the UI displays a clear "Masking failed" status with a non-destructive retry action And CDN/cache never serves any unmasked asset for that image And an error is recorded with a correlation ID available in logs
Exports and Notifications Use Masked Versions by Default
Given a user generates an export (e.g., invoice PDF, compliance report, gallery ZIP) or sends an announcement with image previews When the export or notification is created Then all included images are the masked derivatives with the current styles and strengths applied And email and push notification thumbnails are masked And export bundles and manifests include only masked assets and mark images safe_to_share=true And no URLs or file references in exports or notifications point to the original asset
Mask Editor & Overrides
"As a poster, I want to adjust or remove incorrect masks so that my images remain accurate while still protecting sensitive details."
Description

Provides intuitive tools to review and edit masks before publishing, including add/remove mask, drag-resize, change style and strength, and toggle per-object visibility. Includes undo/redo, keyboard and touch support, zoom, and accessibility compliance. Captures an edit history with timestamps and user IDs for audit. Supports low-confidence highlight prompts and quick actions to confirm or correct detections. Ensures edits propagate to all derivatives and downstream shares.

Acceptance Criteria
Add/Remove Masks and Per-Object Visibility
- Given an uploaded photo with auto-detected masks, when the editor opens, then each detected face/plate/number renders as an editable mask within 1 second and is visible by default. - Given the user clicks Add Mask and drags on the image, when they release, then a new mask is created at the drawn bounds and assigned the current default style and strength. - Given a mask is selected, when the user presses Delete or taps Remove, then the mask is removed and will not appear in any export, derivative, or share. - Given a mask’s visibility toggle is switched off, when the user previews or exports, then that object is not rendered in the output but remains hidden in the editor state. - Given multiple masks exist, when any visibility change is saved, then it persists to storage and is restored on reload of the editor.
Geometry Editing with Zoom
- Given a mask is selected, when the user drags its corner/edge handles, then the mask resizes with 1 px precision, cannot be smaller than 16x16 px, and cannot move beyond image bounds. - Given the user drags inside a selected mask, when moving, then the mask repositions with 1 px precision; with snapping enabled it snaps to edges/guides within 4 px. - Given the user performs pinch (touch) or Ctrl/Cmd + mouse wheel (desktop), when zooming, then the canvas zooms between 10% and 800% smoothly (target 60 fps) without editing latency above 50 ms for move/resize operations. - Given zoom level changes, when editing, then interactive handles maintain a minimum hit target of 44x44 CSS px. - Given the user double-taps (touch) or presses 0/1 keys, when invoked, then the canvas resets to fit (0) or toggles 100% zoom (1).
Style and Strength Adjustments
- Given a mask is selected, when the user switches style (blur, pixelate, box), then the preview updates within 100 ms and subsequent exports use the selected style. - Given blur style, when strength is set via a 0–100 slider, then the rendered blur radius reflects the value within ±5% tolerance and is saved with the mask. - Given pixelate style, when block size is set between 2–50 px, then preview and export apply the exact block size. - Given box style, when color (hex) and opacity (0–100%) are set, then output renders with the specified values. - Given multiple masks are multi-selected, when a style/strength change is applied, then all selected masks update in a single operation and are captured as one undo step.
Undo/Redo, Keyboard Shortcuts, and Touch Gestures
- Given any edit (add/remove/move/resize/style/visibility), when the user presses Ctrl/Cmd+Z or taps Undo, then the last action reverts; when pressing Ctrl/Cmd+Shift+Z or tapping Redo, the reverted action reapplies. - Given an undo is performed, when a new edit occurs, then the redo stack clears. - Given extended usage, when tested, then the editor supports at least 100 sequential undoable actions without state corruption. - Given keyboard operation, when arrow keys are pressed, then the selected mask moves by 1 px; with Shift by 10 px; with Alt/Option the mask resizes by 1 px per keypress using focused handle. - Given touch input, when the user drags with one finger, then the mask moves; pinch zoom uses two fingers; unsupported rotate gestures are ignored without side effects.
Accessibility Compliance (WCAG 2.2 AA)
- Given the editor is opened, when evaluated, then all functionality is operable through a keyboard interface with no keyboard traps and a logical focus order. - Given any interactive control or handle, when focused, then a visible focus indicator is present with at least 3:1 contrast against adjacent colors. - Given text and essential UI elements, when assessed, then color contrast meets or exceeds 4.5:1 for text and 3:1 for graphical components and focus indicators. - Given non-text controls (e.g., mask handles, visibility toggles), when tested with a screen reader, then each exposes an accessible name/role/state via ARIA and is announced meaningfully (e.g., “Mask 3 visibility toggle, off”). - Given touch targets, when measured, then all actionable elements are at least 44x44 CSS px with a minimum 8 px spacing to adjacent targets.
Low-Confidence Prompts and Quick Actions
- Given the detection model flags items below the confidence threshold, when the editor loads, then each low-confidence item is highlighted and listed in a Review panel. - Given a low-confidence item, when the user taps Confirm, then it is promoted to a normal mask and removed from the Review list; when the user taps Dismiss, it is removed entirely. - Given a suggested region, when the user taps Correct, then they can adjust the bounding box and save in a single flow; the final mask replaces the suggestion. - Given unreviewed low-confidence items exist, when the user attempts to publish, then a modal prompts to Review Now or Continue; choosing Continue publishes as-is and logs the override. - Given bulk selection, when Confirm All or Dismiss All is invoked on up to 100 items, then the operation completes within 500 ms and records one audit entry with affected IDs.
Audit Trail and Propagation to Derivatives and Shares
- Given any edit is saved, when checked, then an audit record is appended containing timestamp (UTC ISO 8601), user ID, action type, mask ID(s), and pre/post state diffs. - Given the history view is opened, when paginating or exporting, then entries display in chronological order and can be exported to CSV and JSON. - Given derivatives (thumbnails, feed cards, PDFs) and shared links exist, when a mask edit is saved, then all derivatives and shares reflect the change within 60 seconds or the next render cycle. - Given a previously shared link is visited after an edit, when loaded, then the updated masks render without requiring the sender to regenerate the link. - Given a rollback is performed to a prior history entry, when confirmed, then the editor state and all derivatives/shares update accordingly and a new audit entry records the rollback event.
Community Mask Policies
"As an admin, I want to enforce masking policies and permissions so that our community consistently meets privacy standards without relying on individuals to remember settings."
Description

Adds admin-configurable privacy policies at the community level to enforce auto-masking defaults, minimum blur strength, allowed mask styles, and whether users may view or download originals. Supports role-based exceptions (e.g., compliance officers) and per-channel rules (feed, compliance, payments). Logs policy changes, enforces at publish and export time, and surfaces clear UI messaging so contributors understand what will be masked and why.

Acceptance Criteria
Admin Sets Community Mask Defaults
Given a community admin with Manage Policies permission When they open Settings > Privacy & Masking and configure auto-masking defaults (enabled state, default mask style, minimum blur strength, original view/download toggle) Then the Save action is enabled only when all required fields are valid And the default blur strength cannot be set below the configured minimum; attempting to save shows an inline error and prevents save And allowed mask styles can be restricted to one or more of [blur, pixelate, box]; disallowed styles are not selectable And on save, the policy is persisted and immediately applied to the community configuration
Per-Channel Mask Enforcement at Publish
Given channel-specific policies exist (e.g., Feed: allowed styles=[blur,pixelate], minimum blur strength=6, originals disabled) When a contributor publishes a post with images to that channel Then the system auto-detects faces, license plates, and house numbers and applies masks that meet or exceed the channel policy before publish And if the user's selected mask style is not allowed for the channel, it is replaced by the first allowed style and the user is notified prior to publish And publish is blocked with an error if masking fails to complete; no unmasked media is published And the post displays a notice indicating masking was applied due to community policy
Role-Based Original Access and Overrides
Given a Compliance Officer role is configured with permission to view/download originals and general members are not When a Compliance Officer opens a masked image and selects View Original or Download Original Then the original is displayed/downloaded successfully and the access event is logged with user id, timestamp, item id, and action And when a general member attempts the same actions, the options are hidden in the UI and direct URL access returns HTTP 403; the denied attempt is logged And role-based original access does not alter the masked version visible to other users in the channel
Policy Change Audit Logging
Given a community admin edits any field of the Privacy & Masking policy (defaults, minimum blur strength, allowed styles, originals toggle, per-channel rules, role exceptions) When they save the changes Then an audit entry is recorded containing actor id, timestamp (UTC), changed fields with before/after values, and policy scope (community/channel) And the audit log is viewable by admins with filters by date range, actor, and field; entries are read-only And the audit log can be exported as CSV including all recorded fields; the file contains a header row and one row per entry
Composer UI Policy Messaging
Given a user is composing a post in any channel When the channel has an active masking policy Then the composer displays a policy notice summarizing what will be masked (faces, license plates, house numbers), the allowed mask styles, the minimum blur strength, and whether originals are accessible And the notice is visible before publish and adjacent to the media picker And the notice includes a Learn more link that opens the full policy details in a modal without leaving the composer
Export-Time Mask Enforcement and Access Control
Given a user exports a post/thread or compliance case that includes images When they request an export Then the exported media is masked per the current policy at time of export; no unmasked sensitive details are present unless the user role and policy both allow inclusion of originals And if the user lacks permission to include originals, the export package omits originals and includes only masked media And API requests to include_originals are honored only for roles and channels where originals are permitted; otherwise respond HTTP 403 with error code POLICY_ORIGINALS_FORBIDDEN and log the denial
Protected Originals Storage
"As a compliance officer, I want originals stored securely with strict access controls so that I can access them when necessary without exposing them to everyone."
Description

Stores unmasked originals in encrypted, access-controlled storage separate from masked derivatives, with signed, time-limited access URLs and detailed access logs. Supports configurable retention windows, on-demand purge, and legal hold. Ensures only authorized roles can retrieve originals, and that all default renders across feed, emails, PDFs, and API responses use masked derivatives unless explicit elevated access is granted.

Acceptance Criteria
Originals Encrypted and Isolated Storage
Given an original asset is uploaded When stored Then it is written to an isolated "originals" storage namespace separate from derivatives Given an original asset is stored When at rest Then it is encrypted with AES‑256 using KMS‑managed keys and key rotation occurs at least every 90 days Given any network access to originals storage When transferring data Then TLS 1.2+ is enforced end‑to‑end Given a derivatives‑only service role When attempting to list or read from the originals namespace Then the request is denied with 403 and the attempt is logged Given a key rotation event When new writes occur Then new key versions are used and existing objects remain decryptable
Default Masked Renders Across All Surfaces
Given a user views the community feed When images are displayed Then masked derivatives are rendered instead of originals Given the system sends notification emails containing images When emails are generated Then only masked thumbnails/attachments are included and no direct original URLs are embedded Given a user exports content to PDF When the PDF is generated Then only masked derivatives are included in the document Given an API client requests GET /media/{id} without an explicit original=true parameter When the response is returned Then it contains the masked derivative and no original URL or pointer Given CDN or application caching layers When media is cached Then only masked derivatives are cacheable; originals are never distributed via CDN without a valid signed URL
Controlled Original Retrieval via Signed URL
Given a user with permission originals.read requests an original When authorized Then the API returns a signed URL scoped to that object with a default expiry of 15 minutes and a configurable maximum of 24 hours Given a user without permission originals.read requests an original When processed Then the response is 403 and no signed URL is created Given a signed URL reaches its expiry time When it is used Then access is denied with 403 Given a single‑use policy on signed URLs When the URL is used once Then subsequent requests with the same URL return 403 Given a purge, role revoke, or legal‑hold change occurs When evaluated Then all outstanding signed URLs for affected objects are revoked within 60 seconds
Comprehensive Access Logging
Given any attempt to view, download, or generate a signed URL for an original When the action occurs Then a log entry is recorded with timestamp (UTC), userId/clientId, role, objectId, action, outcome, IP, userAgent, and requestId Given access logs are stored When reviewed by auditors Then entries are immutable (WORM) and retained for at least 2 years Given an auditor with logs.read permission queries by objectId or userId When results are requested Then matching log entries are returned within 5 seconds for the 95th percentile Given distributed systems clock skew When logs are displayed Then entries are ordered by server‑recorded timestamp
Configurable Retention Windows
Given a community sets a retention window of N days for originals When an original exceeds age N and is not on legal hold Then it is queued for deletion and hard‑deleted within 24 hours Given a retention configuration is updated When saved Then the change is logged and applied to new and existing items within 15 minutes Given a retention window of 0 (indefinite) is configured When originals are evaluated Then they are not auto‑deleted and are retained until on‑demand purge or legal hold change
On‑Demand Purge of Originals
Given an admin with permission originals.purge selects one or more originals When a purge is initiated Then the API returns 202 with a jobId and begins deletion immediately Given a purge job is running When it executes Then it deletes targeted originals from primary storage and replicas, revokes any active signed URLs within 60 seconds, and invalidates related caches Given a purge job completes When results are available Then an audit record includes jobId, initiator, objectIds, deleted count, skipped count, and reasons for skips (e.g., LegalHold) Given an original is purged When requests for the asset occur Then the original returns 404 while masked derivatives remain accessible and metadata reflects purged=true
Legal Hold Overrides Deletion
Given an original has legalHold=true When retention expiry is reached or a purge is requested Then deletion is skipped and the reason LegalHold is recorded in logs and job results Given a compliance role updates legal hold on an object When setting or clearing the flag Then a justification is required and the change is logged with userId and timestamp Given legal hold is cleared on an object that exceeds the retention window When the next deletion cycle runs Then the original is deleted within 24 hours
Confidence Thresholds & Review Workflow
"As a moderator, I want low-confidence detections routed to review before publishing so that false positives or misses are corrected without slowing routine posts."
Description

Implements adjustable detection confidence thresholds and a lightweight review gate for low-confidence cases. Displays a pre-publish banner prompting review, queues flagged items for moderators when required by policy, and records reviewer decisions to improve future detection via feedback. Allows justified overrides with reason codes and ensures all outcomes are auditable and reportable.

Acceptance Criteria
Per-Entity Confidence Threshold Configuration
Given I am an admin with Manage Masking permission When I set Face to 0.92, License Plate to 0.85, and House Number to 0.80 and save Then the settings persist and an audit log records before/after values with user and timestamp And detections with confidence >= the configured threshold are auto-masked; detections < the threshold are flagged for review And thresholds accept values between 0.50 and 0.99 in 0.01 increments; invalid inputs are rejected with inline error And default thresholds are 0.90 (Face), 0.85 (Plate), 0.80 (House Number) on first use and a Reset to Defaults action restores them
Pre‑Publish Review Banner for Low‑Confidence Detections
Given a post includes one or more detections below their configured thresholds When the author initiates Publish/Share Then a persistent pre‑publish banner appears summarizing flagged counts by entity type and the lowest confidence value And Publish is blocked until review if policy "Require Review for Low Confidence" is enabled; otherwise Publish remains available with a warning And the banner offers Review Now and Dismiss for now; Dismiss does not publish when the policy requires review And selecting Review Now opens the review panel focused on flagged items
Moderator Queue and Routing Rules
Given community policy "Route Low‑Confidence to Moderator Queue" is enabled When a flagged item is created Then it appears in the Moderation Queue within 10 seconds with status "Pending Review" And items are ordered oldest first and can be filtered by entity type, confidence range, author, and community And only users with the Moderate Masking role can take actions; others have read‑only access And queue updates are real‑time; actions by one moderator immediately reflect for others
Reviewer Decision Capture and Feedback Logging
Given a moderator is reviewing a flagged detection When they submit Approve Mask, Adjust Mask, Remove Mask, or Add Missing Mask Then the decision is saved with entity type, original confidence, bounding box, action taken, pre/post mask snapshot, reviewer, timestamp, and optional notes And the decision generates a feedback record labeled positive or negative for model improvement And the media updates immediately to reflect the decision and the item is removed from the queue
Justified Overrides with Reason Codes
Given a reviewer attempts to override an auto‑mask outcome When they choose an override reason code required by policy and enter a justification of at least 15 characters Then the override is permitted, required fields are validated, and the event is recorded immutably with reason code and justification And overrides are surfaced in analytics and exports with a reason code breakdown
Auditability and Reporting of Outcomes
Given an auditor requests a masking activity export for a date range When the export is generated Then it includes item ID, media ID/URL, entity type, detector confidence, threshold at decision time, outcome (auto‑mask/approved/adjusted/removed/added), reviewer (if any), reason code, timestamps, and policy snapshot And each publish event stores a hash of the final mask set and a link to the complete decision trail And audit records are append‑only; attempts to delete or edit are blocked and logged And dashboards display counts and rates by outcome, reviewer, and confidence bands with filterable time ranges
Performance, Monitoring, and Resilience
"As a user, I want fast and reliable processing so that posting photos feels seamless even during busy community events."
Description

Meets strict performance targets (e.g., p95 under 1.5 seconds for standard images) with background processing for large files and progress indicators in the UI. Includes retry logic, rate limiting, and graceful degradation to low-resolution previews if needed. Exposes metrics for detection latency, error rates, and masking coverage with alerts for anomalies. Scales horizontally to handle spikes from bulk uploads and community events.

Acceptance Criteria
p95 Latency for Standard Images
Given 1,000 standard images (<=12MP, <=6MB) are uploaded evenly over 10 minutes under nominal load, When Smart Auto‑Mask processes them synchronously, Then p95 time from upload completion to masked preview visible is <=1.5s and p99 <=2.5s per region.
Asynchronous Processing and Progress for Large Images
Given an image >12MP or >6MB is uploaded, When processing begins, Then the job runs in the background and the UI shows a non‑blocking progress indicator with states (Queued -> Processing -> Finalizing), percent complete, and ETA updating at least every 1s, And progress state persists across page navigation/refresh and restores within 500ms upon return, And the final masked asset replaces the preview without requiring re‑upload.
Transient Failure Retries and Idempotency
Given a transient error (timeout or HTTP 5xx) occurs while submitting or executing a masking job, When the system retries, Then it uses exponential backoff with jitter (approximately 0.5s, 1s, 2s, 4s, 8s) up to 5 attempts within 60s, And an idempotency key per image ensures at most one completed masked asset and suppresses duplicate processing, And upon final failure the UI displays a clear retry option and logs include error_code and attempt_count.
Rate Limiting Enforcement and UX
Given a user exceeds 60 image uploads in any rolling 60s window or a community exceeds 500 in 60s, When the threshold is crossed, Then the API returns HTTP 429 with a valid Retry‑After header, excess requests are not enqueued, and already‑accepted uploads continue processing, And the UI shows a throttling message and auto‑retries after the Retry‑After interval.
Graceful Degradation with Low‑Resolution Masked Previews
Given queue_depth > 10,000 or detection latency p95 > 1.5s sustained for 5 minutes, When new images are uploaded, Then a low‑resolution masked preview (<=1280px longest edge) is shown within 700ms at p95 with minimum mask strength (blur sigma >= 8 or pixel size >= 12px) for all detected regions, And the final high‑resolution masked asset replaces the preview when ready without any intermediate exposure of unmasked sensitive details.
Metrics Exposure and Alerting for Performance and Coverage
Given the service is running in production, When metrics are scraped, Then the system exposes per‑region and per‑model metrics: detection_latency_seconds (histogram), masking_error_rate (counter/gauge), masking_coverage_ratio (gauge), queue_depth (gauge), job_retry_count (counter), and rate_limit_hits (counter), And alerts fire within 2 minutes when any of the following hold: detection_latency_seconds_p95 > 1.5 for 5 minutes; masking_error_rate > 2% for 5 minutes; masking_coverage_ratio_p50 < 0.95 for 10 minutes; queue_depth > 10,000 for 10 minutes; with routing to on‑call.
Horizontal Scaling Under Burst Load
Given a burst of 10,000 standard images is uploaded within 10 minutes in a single region, When autoscaling is enabled, Then the system scales out to maintain masked preview p95 <= 1.5s, error rate <= 2%, and average queue wait <= 5s, with CPU utilization per node <= 80%, And 99% of large‑image jobs (>12MP) complete within 5 minutes of upload via background processing.

Confidence Heatmap

Overlays color cues on each image to show high and low detection confidence so you know exactly where to double‑check. One tap jumps to flagged zones for quick manual touch‑ups, reducing misses and building trust that nothing sensitive slipped through.

Requirements

Heatmap Overlay Rendering
"As a community manager, I want to see confidence overlays on images so that I can instantly spot areas that may need manual review before posting."
Description

Render a semi-transparent color-coded overlay on images attached to announcements, payments, and compliance records that indicates detector confidence levels (e.g., green=high, yellow=medium, red=low). The overlay must align with zoom/pan, respect image rotation and EXIF orientation, and support both bounding boxes and pixel masks. Provide a toggle to show/hide the heatmap and an on-canvas legend explaining color ranges. The implementation must be non-destructive (does not alter the source image), work on web, iOS, and Android, and integrate with the existing media viewer and post/composer flows. Ensure support for large images (up to 20MP), maintain visual fidelity across DPRs, and avoid exposing raw sensitive content—only derived detector metadata and masks are transmitted to the client.

Acceptance Criteria
Overlay Color Mapping and On-Canvas Legend
Given an image with detector confidence scores and heatmap enabled When the overlay renders Then areas with high confidence display as green, medium as yellow, and low as red, and the colors match the legend keys Given the overlay is visible When the user views the legend Then the legend shows color swatches labeled with confidence ranges and is readable across DPR 1x–3x Given the overlay is visible When inspecting underlying content Then the image remains visible through the semi-transparent overlay
Heatmap Show/Hide Toggle
Given an image opened in the media viewer or composer and the Heatmap toggle is off When the user enables the Heatmap toggle Then the overlay and legend appear without modifying the source image Given the Heatmap toggle is on When the user disables it Then all overlay elements and the legend are hidden and the underlying image view is unchanged
Alignment with Zoom, Pan, Rotation, and EXIF Orientation
Given an image with EXIF orientation metadata and associated detector masks/boxes When the image is loaded Then the overlay respects the normalized orientation and aligns to within 1 device pixel at 100% zoom Given the user pans or pinches to zoom When the image transforms Then the overlay remains anchored with no drift or tearing and maintains ≤1 device-pixel alignment error Given the user rotates the image (where supported) When rotation is applied Then the overlay rotates identically and remains aligned to within 1 device pixel
Support for Bounding Boxes and Pixel Masks
Given detector outputs include pixel masks When the heatmap is enabled Then masks render with semi-transparent confidence-based coloring and blend correctly over the image Given detector outputs include bounding boxes When the heatmap is enabled Then boxes render with stroke color keyed to confidence and remain visible at all zoom levels Given both masks and boxes are provided When rendered together Then both display concurrently without flicker, with masks beneath box strokes, and image interactions (pan/zoom) remain responsive
Non-Destructive Rendering and Privacy-Safe Data Flow
Given heatmap is enabled on an image When the image is downloaded/exported/saved Then the saved file bytes match the original source (no overlay baked in) Given network traffic for heatmap data is inspected When requests and responses are analyzed Then only derived detector metadata (e.g., mask arrays, polygons, box coordinates, confidence values) are transmitted and no raw cropped pixel data or auxiliary images are sent Given client storage is reviewed When caches and files are inspected Then only derived metadata and overlay assets are stored; the original image asset remains unchanged
Cross-Platform Parity in Viewer and Composer
Given supported platforms (web, iOS, Android) When opening the media viewer for posts, payments, and compliance records Then the Heatmap toggle and overlay are available and behave consistently on each platform Given composing a new post with an image attachment When opening the image preview in the composer Then the user can enable/disable the heatmap and view the legend before publishing, and the same functionality is available after publishing in the viewer Given the same image and detector data When compared across platforms Then the overlay visual output (colors and alignment) matches within 1 device pixel relative to the underlying image
Large Image Support and Visual Fidelity Across DPRs
Given a 20MP image (≈5472×3648) with corresponding detector data When enabling the heatmap on devices with DPR 1x, 2x, and 3x Then the app does not crash or trigger ANR and the overlay remains crisp at 100% zoom with ≤1 device-pixel alignment error Given the user zooms beyond 100% When inspecting overlay edges and small features Then overlay edges align to the underlying image pixel grid without visible misregistration or excessive blurring
Flagged Zones Navigation
"As a board member, I want one-tap navigation between flagged zones so that I can quickly review and fix potential issues without losing my place."
Description

Enable one-tap/click navigation between detected zones requiring attention. Provide Next/Previous controls and keyboard shortcuts (e.g., N/P) that auto-zoom to the selected area and display its label, confidence score, and quick actions (Redact, Mark Safe, View Details). Maintain a checklist of unresolved flags and show an "All clear" state when finished. Persist navigation state in drafts, support touch gestures, and ensure navigation works consistently from the feed, composer, and media lightbox without losing context.

Acceptance Criteria
One-Tap Navigation Auto-Zoom to Flagged Zone
Given an image with one or more flagged zones visible on the confidence heatmap When the user taps or clicks a highlighted flagged zone Then the viewport auto-zooms to fit and center the selected zone within the media bounds And the selected zone is visually focused (e.g., outlined) while non-selected zones are de-emphasized And the zone’s label and confidence score are displayed adjacent to the zone And the quick actions Redact, Mark Safe, and View Details are visible and enabled And the interaction responds within 100 ms and the zoom transition completes within 400 ms
Next/Previous Controls and Keyboard Shortcuts (N/P)
Given at least two unresolved flagged zones exist on the image When the user activates Next or presses the "N" key while focus is within the media viewer (and not inside a text input) Then the next unresolved flagged zone becomes selected, auto-zoomed, and annotated with its label, confidence, and quick actions And the selection wraps from the last unresolved zone to the first unresolved zone And a visual indicator shows the position (e.g., 2 of 5) When the user activates Previous or presses the "P" key Then the previous unresolved flagged zone becomes selected with the same behaviors And when no unresolved zones remain, the Next/Previous controls and N/P shortcuts are disabled and a hint indicates All clear
Quick Actions on Selected Flagged Zone
Given a flagged zone is selected When the user taps Redact Then a redaction mask is applied to the selected zone and the zone is marked resolved and removed from the unresolved list And the UI updates within 200 ms to reflect the change When the user taps Mark Safe Then the zone is marked resolved without applying redaction and is removed from the unresolved list When the user taps View Details Then a details view opens showing zone metadata (label, confidence, coordinates, detection source) without losing the current selection context And if any action fails to persist to the server, an error message is shown and the UI state is reverted
Checklist of Unresolved Flags and All Clear State
Given flagged zones requiring attention exist Then a checklist displays each unresolved zone with its label and confidence score And selecting any checklist item navigates to and selects that zone, auto-zooming it into view And resolving a zone (Redact or Mark Safe) removes it from the checklist and updates the unresolved count badge And when the last unresolved zone is resolved, the checklist shows an All clear state with a confirmation indicator and disables Next/Previous navigation And if no flagged zones exist initially, the checklist immediately shows All clear
Persist Navigation State in Drafts
Given the user has an in-progress draft with one or more flagged zones When the user navigates away from the composer or closes the media lightbox Then the navigation state (selected zone, zoom level, scroll position, and unresolved checklist state) is saved with the draft When the user re-opens the same draft Then the previously saved navigation state is restored within 1 second and the user resumes at the same zone and zoom context And if the underlying media changed (dimensions or zones), the state gracefully resets and the user is notified of the reset
Context-Preserving Navigation Across Feed, Composer, and Lightbox
Given a user opens media from the feed into the lightbox or composer When the user selects or navigates between flagged zones Then the selected zone and navigation position persist when switching between feed, composer, and lightbox views for the same media And returning to the feed restores the user to the originating post position without losing scroll context And all navigation controls (Next/Previous, N/P shortcuts) behave consistently across these entry points
Touch Gesture Support for Flagged Zone Navigation
Given a touch-capable device When the user taps a flagged zone Then it becomes selected and auto-zooms with label, confidence, and quick actions shown When the user swipes left or right on the media Then the selection moves to the next or previous unresolved zone respectively with the same behaviors And touch targets for quick actions are at least 44x44 px and have visible touch feedback And pinch-to-zoom does not break the selected zone overlay or checklist state And gesture handling does not conflict with system back or parent scroll (e.g., horizontal swipes are captured within the media viewer)
Confidence Threshold Controls
"As an admin, I want to set and adjust confidence thresholds so that our community balances thoroughness with review time."
Description

Provide user-adjustable controls to tune confidence thresholds per detector type (e.g., Faces, Plates, PII). Include a slider and presets (Show All, Only Low Confidence, Hide) with admin-configurable community defaults and per-user persistence. Update the color scale and zone visibility in real time as thresholds change. Enforce guardrails for high-severity detections (cannot be fully hidden without explicit override). Expose settings within the media viewer and global preferences, with multi-tenant admin controls for default policies.

Acceptance Criteria
Per-Detector Threshold Slider Adjusts Heatmap in Viewer
Given a media item contains detections for Faces, Plates, and PII When the user adjusts the Faces threshold slider from 60% to 80% Then Face zones with confidence < 80% are hidden or styled as low-confidence and Face zones >= 80% are shown as high-confidence And the Plates and PII thresholds remain unchanged And the slider supports a range of 0–100% in 1% increments and snaps to the nearest step
Preset Buttons Apply Correct Filters
Given the user is viewing detections for Plates with a threshold of 70% When the user selects the "Show All" preset Then all Plate zones are visible and color-scaled across the full 0–100% range When the user selects the "Only Low Confidence" preset Then only Plate zones with confidence < 70% are visible and Plate zones >= 70% are hidden When the user selects the "Hide" preset Then all Plate zones are hidden except where guardrails prevent hiding
Admin-Configurable Community Defaults
Given an admin sets community defaults: Faces threshold 75%, Plates threshold 60%, PII threshold 80%, preset "Only Low Confidence" When a new user in that community opens the media viewer for the first time Then those defaults are applied for each detector and preset And the global preferences screen displays the same defaults And existing users with previously saved preferences are not overwritten by the new defaults
Per-User Preference Persistence Across Sessions and Devices
Given a user sets PII threshold to 65% and selects "Only Low Confidence" in the media viewer When the user signs out and signs back in on another device Then the PII threshold remains 65% and the preset remains "Only Low Confidence" And opening global preferences shows the same values And using "Reset to default" restores the current community defaults for that user
Real-Time Heatmap Color and Visibility Updates
Given the detector threshold slider is being adjusted in the media viewer When the slider value changes Then heatmap colors and zone visibility update within 200 ms without reloading the media item And updates occur locally without requiring a network roundtrip
Guardrails and Explicit Override for High-Severity Detections
Given the admin has marked Faces and PII as high-severity with guardrails enabled When the user attempts to hide high-severity detections via any control (e.g., selecting the "Hide" preset) Then high-severity zones remain visible and the hide action is blocked And the user is prompted with an explicit override dialog requiring confirmation and a reason And if the user lacks override permission, the override is not allowed and an explanatory message is shown And any successful override creates an audit log entry with user, timestamp, detector type, media item ID, and reason
Multi-Tenant Policy Scope and Effective Defaults
Given a super-admin sets organization-wide defaults and a community admin sets community-specific defaults for Plates When a user in that community opens preferences or the media viewer Then the effective default for Plates is the community default, while other detectors fall back to the organization default And selecting "Reset to default" restores the effective defaults based on the user's community And users in other communities are unaffected by the community-specific defaults
Detector API Integration
"As a user, I want detections to load quickly and consistently so that I can trust the tool and finish reviews on time."
Description

Integrate with a backend detection service that returns a standardized payload per image, including detector type, label, bounding box/mask geometry, confidence score, severity, and model/version metadata. Support batch processing, idempotency keys, retries with exponential backoff, and a circuit breaker for degraded external dependencies. Target end-to-end p95 latency under 500 ms for 5 MB images. Secure communications with OAuth2 and scoped tokens; store artifacts in object storage with lifecycle TTL. Provide async backfill with UI notification if synchronous processing times out, and expose a schema version for forward compatibility.

Acceptance Criteria
Standardized Detection Payload for Heatmap Rendering
Given a 5 MB image is submitted for detection via the Detector API When the backend detection response is received Then the response validates against the published JSON Schema with required fields: detector_type, label, geometry (bbox or mask), confidence [0.0–1.0], severity, model, model_version, schema_version And geometry coordinates map to the source image dimensions with no out-of-bounds values And confidence values are preserved as floating-point numbers in [0,1] And the normalized detections are returned in the API response for rendering And the original response and derived overlays are persisted to object storage with the configured lifecycle TTL
Batch Processing with Idempotency Key
Given a batch request contains multiple images and a unique idempotency key When the same batch payload is retried with the same idempotency key within the idempotency window Then the system returns the original response and does not create duplicate processing jobs or artifacts And metrics record a single processed batch and deduplicated retries When the same payload is sent with a different idempotency key Then a new batch is processed and artifacts are created
Resilience: Retries, Backoff, and Circuit Breaker
Given the external detection service returns transient errors (HTTP 5xx or timeouts) When the Detector API calls the service Then it retries using exponential backoff with jitter up to the configured maximum attempts And total retry duration does not exceed the configured ceiling And after the configured consecutive-failure threshold the circuit opens and subsequent calls are short-circuited until half-open probes succeed And all retry and circuit events are emitted as structured logs and metrics
Secure OAuth2 with Scoped Token Enforcement
Given API requests include OAuth2 access tokens with various scopes (detections:read, detections:write) or are missing/invalid When accessing read and write detection endpoints Then requests with sufficient scope succeed (200/202) and insufficient/invalid tokens are rejected (401/403) without leaking sensitive data And all communications occur over TLS 1.2+; insecure connections are refused And tokens are validated for issuer, audience, expiry, and scope
Performance: p95 Latency ≤ 500 ms for 5 MB Images
Given a controlled test workload of 5 MB images processed synchronously When at least 1000 requests are executed under nominal conditions Then end-to-end p95 latency per request (from API receipt to response) is ≤ 500 ms And latency and throughput metrics are published to monitoring with timestamps and percentiles
Async Backfill with UI Notification on Sync Timeout
Given synchronous processing for an image exceeds the configured timeout When the timeout is reached Then the API responds with HTTP 202 Accepted and a job_id and enqueues the image for async backfill And the UI is notified of 'processing' state via the configured channel within the notification SLA When async backfill completes Then the UI receives a 'completed' notification with the detections payload and the heatmap renders without manual refresh
Forward-Compatible Schema Versioning
Given the backend includes a schema_version in its response When the client receives a response with a newer minor schema_version Then required fields are parsed successfully and unknown fields are ignored without error When a major schema_version change causes required fields to be missing or incompatible Then the API returns a clear error indicating unsupported schema and logs an actionable message with the observed version And per-version JSON Schemas are published and discoverable
Review Actions & Audit Logging
"As a compliance officer, I want a complete audit of my review actions so that we can demonstrate due diligence and improve detection quality."
Description

Record all manual review actions taken on heatmap zones, including redactions applied, zones marked safe, threshold overrides, and skips. Persist action metadata with user, timestamp, image reference, parent record (post/payment/compliance), detector type, and model version. Display an activity timeline within the item, emit analytics events for false positives/negatives to inform model improvements, and provide CSV export for board audits. Ensure privacy by storing only necessary metadata and redaction masks while honoring role-based access.

Acceptance Criteria
Log All Review Actions With Metadata
Given an authenticated reviewer is viewing a Confidence Heatmap for a specific image under a post, payment, or compliance item When the reviewer applies a redaction to a detected zone Then a single immutable audit entry is created with fields: action_type=redact, user_id, timestamp_utc (ISO 8601), image_id, zone_id, parent_record_id, parent_record_type, detector_type, model_version, mask_reference, pre_action_confidence, and review_session_id, and is visible in the item timeline within 2 seconds Given the reviewer marks a zone as safe When the action is submitted Then an audit entry is created with fields: action_type=mark_safe, user_id, timestamp_utc, image_id, zone_id, parent_record_id, parent_record_type, detector_type, model_version, pre_action_confidence Given the reviewer overrides the detection threshold for the item When the new threshold is saved Then an audit entry is created with fields: action_type=threshold_override, user_id, timestamp_utc, image_id, parent_record_id, parent_record_type, detector_type, model_version, previous_threshold, new_threshold Given the reviewer skips a zone When the skip is confirmed Then an audit entry is created with fields: action_type=skip, user_id, timestamp_utc, image_id, zone_id, parent_record_id, parent_record_type, detector_type, model_version, reason (nullable) Given a transient network failure during logging When the client retries with an idempotency key for the action Then only one audit entry is stored per unique action and no duplicates appear in the timeline
Activity Timeline Within Item
Given a user with role Reviewer, Manager, or Board Admin opens an item with a Confidence Heatmap When they navigate to the Activity timeline Then entries are listed in reverse-chronological order, display action_type, user_display_name, timestamp (viewer’s locale), and link to image/zone; clicking an entry focuses the corresponding zone on the image Given the timeline is loaded When the user applies filters by action_type, user, and date range Then only matching entries are shown and the state is preserved for deep-linking within role permissions Given a user with role Resident or an unauthorized user When they attempt to access the timeline Then access is denied with 403 and no audit data is returned
Emit Analytics for False Positives/Negatives
Given a detected zone is marked safe by a reviewer When the action is saved Then a false_positive analytics event is emitted with properties: detector_type, model_version, confidence, zone_area_px, parent_record_type, parent_record_id, image_id, zone_id, reviewer_role, event_timestamp_utc, and is delivered to the analytics pipeline within 5 seconds (p95) Given a manual redaction is added to an area without a corresponding model-detected zone When the action is saved Then a false_negative analytics event is emitted with properties: detector_type, model_version, parent_record_type, parent_record_id, image_id, mask_reference, reviewer_role, event_timestamp_utc, and is delivered within 5 seconds (p95) Given analytics delivery fails temporarily When retries execute Then events are retried with exponential backoff for at least 24 hours and deduplicated by event_id
CSV Audit Export With Privacy and Filters
Given a Manager or Board Admin selects a date range, parent_record_type filter, and users When they request a CSV export for an item or for the community Then a CSV is generated containing only authorized records with headers: action_id, timestamp_utc, user_id, user_role, parent_record_type, parent_record_id, image_id, action_type, zone_id, detector_type, model_version, confidence, previous_threshold, new_threshold, mask_reference, and excludes any pixel data or redacted content text Given the export is requested for up to 100,000 records When generation completes Then the file is available within 60 seconds (p95) via a secure download and contains all matching rows Given a Reviewer or Resident attempts to export When the request is made Then the system returns 403 and no file is produced Given the CSV is opened When data integrity is checked Then all timestamps are UTC ISO 8601, required IDs are non-null, and the row count matches the number of returned records
Role-Based Access Controls for Audit Data
Given role permissions are configured as Resident < Reviewer < Manager < Board Admin When accessing the audit timeline or requesting CSV export Then permissions are enforced: Residents=no access; Reviewers=create actions and view the item timeline; Managers and Board Admins=view all item timelines and export within their community Given an unauthorized access attempt to audit data When it occurs Then the system responds 403, emits a security audit event with user_id, attempted_resource, and timestamp_utc, and does not disclose whether records exist
Privacy-Conscious Storage of Audit Data
Given an audit entry is stored When data is persisted Then only necessary metadata and redaction masks are stored; no unredacted image bytes or extracted sensitive text are stored in the audit record Given a redaction mask is persisted When stored Then it is saved as a reference linked to the image, and retrieving the mask requires the same role-based access as the parent item Given data retention and deletion policies When a data subject request or retention threshold is reached Then audit records are retained or deleted per policy, and deletions are themselves logged with user_id and timestamp_utc
Accessibility & Legend
"As a user with color-vision deficiency, I want accessible heatmap cues and a clear legend so that I can interpret detection confidence accurately."
Description

Deliver a colorblind-friendly palette, high-contrast mode, and optional pattern overlays that do not rely on color alone to convey confidence. Provide a persistent, localized legend mapping color/patterns to confidence ranges and severity. Ensure full keyboard operability and screen-reader support with concise announcements (e.g., number of zones, current zone details, available actions). Meet WCAG 2.1 AA standards for contrast, focus management, and non-color cues across web and mobile platforms.

Acceptance Criteria
Colorblind-Safe Heatmap Cues
Given the heatmap is displayed and color vision deficiency simulation is set to protanopia, deuteranopia, or tritanopia, When viewing confidence levels, Then high/medium/low regions are visually distinguishable without relying on hue alone. Given pattern overlays are enabled, When confidence levels are applied, Then each level uses a distinct, non-ambiguous pattern and label, and patterns remain visible at 100% zoom without obscuring content (overlay opacity ≤ 30%). Given color-only mode is used, When testing non-text contrast, Then each confidence region meets WCAG 2.1 AA 1.4.11 (≥ 3:1) against adjacent imagery/background. Given the app loads, When the user toggles patterns on/off, Then the setting persists for the session and does not alter the underlying confidence data.
High-Contrast Mode Compliance
Given the OS/browser prefers-contrast setting is "more" or the in-app High Contrast toggle is enabled, When the heatmap renders, Then text and legend text meet WCAG 2.1 AA 1.4.3 (≥ 4.5:1 for normal text; ≥ 3:1 for large text) and graphical/non-text overlays meet 1.4.11 (≥ 3:1). Given a user toggles High Contrast on or off, When the UI updates, Then focus remains on the toggle control, no unexpected scroll occurs, and all controls remain operable. Given High Contrast is enabled, When viewing the image, Then confidence cues remain discernible via both contrast and non-color cues (patterns/labels) without loss of information.
Persistent Localized Legend for Confidence and Severity
Given the heatmap loads, When the UI renders, Then a persistent legend is visible by default, docked without covering more than 20% of the image viewport, and remains visible during pan/zoom. Given the app locale is changed, When the legend re-renders, Then all labels (confidence bands, severity terms, actions) are localized and numerals use locale-appropriate formatting. Given confidence bins are configured (e.g., 0–25, 26–75, 76–100), When the legend displays ranges, Then ranges are numeric, non-overlapping, cover 0–100% inclusively, and map 1:1 to colors/patterns. Given a screen reader is enabled, When focus moves to the legend, Then it has an accessible name and each swatch announces color name (or pattern), numeric range, and severity label.
Full Keyboard Operability and Focus Management
Given a keyboard-only user, When navigating from the heatmap container, Then all interactive elements (heatmap, legend, pattern toggle, high-contrast toggle, "Jump to next flagged zone", zone actions, close) are reachable in a logical order with no traps. Given keyboard navigation, When a control receives focus, Then a visible focus indicator is present and meets WCAG 2.1 AA 1.4.11 (≥ 3:1 contrast versus adjacent pixels). Given the user activates "Jump to next flagged zone" via Enter/Space, When the next zone is focused, Then the viewport centers on the zone and the previous focus is preserved for return navigation; pressing Esc exits zone focus. Given a modal or popover is opened from a zone, When it is closed, Then focus returns to the invoking control and not to the page top.
Screen Reader Announcements and Zone Summaries
Given a screen reader is enabled, When the heatmap initializes with N flagged zones, Then a polite live announcement states the heatmap is loaded and indicates N zones and how to navigate. Given focus enters a zone, When the zone summary is read, Then it announces position (e.g., Zone X of N), confidence value and band (e.g., 82%, High), severity label, and available actions, without duplicate or extraneous verbosity. Given the user activates "Jump to next flagged zone", When focus moves, Then the new zone’s summary is announced within 1 second. Given the legend is focused, When reading swatches, Then each swatch has an accessible name describing its color/pattern and the numeric confidence range and severity.
Mobile Accessibility (VoiceOver/TalkBack) and Touch Targets
Given iOS VoiceOver or Android TalkBack is enabled, When navigating the heatmap and controls, Then all controls are reachable via swipe, properly labeled with roles/states, and actionable via double-tap. Given mobile interaction, When tapping controls, Then all tappable targets (legend toggle, pattern toggle, high-contrast toggle, jump control, zone actions) meet minimum size 44x44 dp and have sufficient spacing to avoid accidental activation. Given system font scale up to 200%, When viewing the heatmap and legend, Then content reflows without loss of information or functionality (WCAG 2.1 AA 1.4.10 Reflow), and legend remains accessible. Given system high-contrast/inverted colors settings, When the app renders, Then confidence cues remain perceivable with non-color cues and required contrast is maintained.
Performance & Caching
"As a user on a mid-tier device, I want smooth, responsive heatmap interactions so that reviews are fast and frustration-free."
Description

Optimize client rendering using GPU-accelerated canvas/WebGL with graceful fallbacks. Tile and stream large overlays, prefetch detector data on media open, and lazy-load offscreen assets to maintain smooth interactions under 16 ms per frame during pan/zoom on mid-tier devices. Cache detector payloads by image revision with proper invalidation on edits or re-uploads. Instrument telemetry for render time, memory usage, and error rates, and alert when performance degrades beyond thresholds.

Acceptance Criteria
GPU-Accelerated Rendering with Graceful Fallback
- Given a device with WebGL2 support, When the heatmap overlay is displayed, Then the renderer initializes a WebGL context and draws frames using GPU shaders. - Given WebGL initialization fails or is unsupported, When the heatmap overlay is displayed, Then the renderer falls back to 2D Canvas and the UI remains responsive with initialization under 200 ms. - Given the same image and detector payload, When rendered via WebGL and via 2D Canvas, Then per-channel color/alpha differences are ≤ 1% RMSE at 1x scale. - Given a runtime WebGL context loss, When it occurs during interaction, Then the renderer switches to 2D Canvas within 500 ms and the interaction remains continuous. - Given a mid-tier device, When switching between GPU and fallback, Then peak memory for the viewer process stays ≤ 250 MB and no fatal errors are logged.
Tiled Overlay Streaming and Viewport Budgeting
- Given an image larger than 4096x4096, When panning or zooming, Then only tiles intersecting the viewport plus a 1-tile margin are decoded and drawn. - Given a tile becomes offscreen beyond the margin, When its request or decode is in progress, Then the work is canceled within 100 ms and related network requests are aborted. - Given network throttled to 10 Mbps with 100 ms RTT, When zooming rapidly for 5 seconds, Then blank tiles persist ≤ 150 ms and dropped frames are ≤ 5%. - Given 256 px tiles, When in steady state, Then concurrent tile decodes are limited to 2x logical CPU cores and in-flight GPU uploads to ≤ 4. - Given a tile is revisited within 2 minutes, When it re-enters the viewport, Then it is served from in-memory cache with a hit rate ≥ 80%.
Prefetch Detector Payloads on Media Open
- Given a user opens an image, When the viewer loads, Then the detector payload for the current revision is requested within 200 ms and is ready before first interaction. - Given the user navigates away before payload arrival, When navigation occurs, Then the prefetch request is canceled within 100 ms. - Given an image has no detector payload available, When prefetch is attempted, Then the viewer shows a heatmap skeleton state within 100 ms and avoids retry loops. - Given a preloaded payload exists, When the user opens the same image again within 10 minutes, Then the payload is read from cache without a network request. - Given bandwidth is constrained, When prefetch competes with visible tile requests, Then prefetch is deprioritized and LCP impact is ≤ 50 ms.
Lazy-Load Offscreen Assets
- Given the viewer opens, When loading assets, Then only critical code and data for first heatmap draw are loaded and non-critical assets defer until after first frame. - Given tiles or UI modules are offscreen, When they are outside the viewport, Then they are not requested until within 300 px of the viewport edge. - Given a 3G Fast connection (≈1.6 Mbps), When the viewer opens, Then time-to-first-heatmap-draw is ≤ 1200 ms and main-thread long tasks > 50 ms are ≤ 1 during load. - Given a lazy-loaded module enters the viewport, When intersection occurs, Then it renders within 150 ms. - Given 6 or more lazy-load requests are queued, When fetching, Then in-flight requests are capped at 6 and frame budget overrun is ≤ 10%.
Pan/Zoom Performance Budget (16 ms per Frame)
- Given a mid-tier device (4-core CPU, integrated GPU), When continuously panning or pinch-zooming for 10 seconds, Then average frame render time is ≤ 16 ms and P95 is ≤ 24 ms. - Given three overlays are active, When interacting on a mid-tier device, Then CPU utilization averages ≤ 70% and no single main-thread task exceeds 50 ms. - Given GPU backend is active, When rendering, Then the render queue latency is ≤ 4 frames measured via requestAnimationFrame timestamps. - Given the viewer is idle, When no interactions occur for 5 seconds, Then RAF callbacks pause and re-renders are ≤ 1 per second. - Given memory pressure from other tabs, When panning and zooming, Then viewer memory stays ≤ 200 MB and frames lost to GC are ≤ 1% of total.
Cache by Image Revision with Correct Invalidation
- Given imageId A revision r1, When the detector payload is fetched, Then it is cached under key (A,r1) with a TTL of ≥ 24 hours subject to eviction. - Given an edit produces revision r2, When opening the image at r2, Then (A,r1) is not used and a fresh fetch for (A,r2) occurs. - Given a re-upload with new revision r3, When opened, Then (A,r3) is cached separately from r1/r2 even if binaries match. - Given the user triggers "Re-run detector", When invoked, Then the cache entry for the current revision is invalidated within 100 ms and the next fetch bypasses cache. - Given the cache exceeds 100 MB, When eviction runs, Then least-recently-used entries are removed and session hit rate remains ≥ 60% over 1 hour.
Performance Telemetry and Degradation Alerts
- Given the viewer session starts, When initializing, Then telemetry for frame times (avg/P95), memory high-water, renderer mode, and error counts is recorded with ≥ 1% sampling. - Given a session ends or 5 minutes of idle, When flushing, Then telemetry is sent with ≤ 1% loss as measured by backend acknowledgments. - Given P95 frame time exceeds 24 ms for 3 consecutive minutes across ≥ 50 mid-tier sessions, When detected by the backend, Then an alert is emitted to on-call within 5 minutes. - Given a catastrophic client error (unrecovered WebGL context loss or OOM), When it occurs, Then a high-severity event is logged within 2 seconds including imageId and revision. - Given telemetry is disabled by the user, When the viewer runs, Then no metrics are sent and all features remain functional.

Timed Reveal

Lets authorized reviewers temporarily view originals behind a secure gate with a required reason, automatic expiry, and watermarked “For Review” overlay. Every reveal is logged and auto‑re‑masked—minimizing privacy risk while keeping investigations efficient and audit‑ready.

Requirements

Role-Gated Timed Reveal Access
"As a compliance officer, I want to temporarily view masked originals only when I’m authorized so that I can investigate issues without exposing resident data unnecessarily."
Description

Implement granular, role-based permissions that restrict who can initiate a Timed Reveal on sensitive originals (e.g., compliance submissions, payment details, resident attachments) within Duesly’s feed and detail views. Enforce an authentication freshness check (e.g., recent password re-entry or 2FA) prior to reveal, and verify community-specific policies and sensitivity labels before granting view-only access. The flow presents a gated modal when a user clicks Reveal, validates authorization and policy, and then opens a watermark-enforced viewer. This reduces unauthorized exposure while fitting seamlessly into existing Duesly roles and communities.

Acceptance Criteria
Role- and Community-Scoped Reveal Availability
Given I am authenticated with the Timed Reveal permission in community X When I view sensitive items in the feed or detail views of community X Then I see an enabled Reveal control on those items Given I lack the Timed Reveal permission in community X When I view the same items Then the Reveal control is not visible (or is disabled) and any direct-reveal URL returns 403 and is logged Given I am authorized in community A only When I view items in community B Then I cannot see or use Reveal for community B items and any attempt is denied and logged
Authentication Freshness Gate Before Reveal
Given my last strong authentication is older than the configured freshness window When I click Reveal Then I am required to complete password re-entry or 2FA before proceeding Given I successfully complete the freshness check When I submit the gate Then the reveal proceeds; otherwise the reveal is blocked and the attempt is logged Given my last strong authentication is within the freshness window When I click Reveal Then I am not prompted again and the reveal continues
Community Policy and Sensitivity Label Enforcement
Given a community policy or sensitivity label prohibits reveal for the item When I attempt to reveal Then access is denied with a clear inline reason and the attempt is logged Given policy allows reveal for my role and the item’s sensitivity label When I attempt to reveal Then the reveal proceeds Given policy requires a more recent authentication freshness than my current state When I attempt to reveal Then I am prompted to refresh authentication before proceeding
Required Reason Capture and Validation
Given I click Reveal When the gate modal opens Then I must provide a non-empty reason to proceed Given the reason field is empty or whitespace When I submit Then validation prevents submission with an inline message Given I submit a valid reason When the reveal starts Then the reason is stored with the audit record for the session
Watermarked, View-Only Reveal Viewer
Given a reveal is granted When the viewer opens Then a visible "For Review" watermark is displayed across the content for the duration of the session Given the viewer is open When I attempt to download, print, export, or edit the content Then those actions are disabled or blocked and I am kept in view-only mode Given the viewer is visible When I take a screenshot Then the watermark remains visible in the captured content (overlay persists)
Auto-Expiry and Automatic Re-Masking
Given a reveal is in progress When the configured expiry is reached or I navigate away, close, or refresh Then the sensitive content is automatically re-masked and the session ends Given a reveal session has expired When I return to the item or use the browser back button Then the content remains masked and I must restart the gated flow to view again Given a reveal is active When the countdown approaches expiry Then I see a time-left indicator and any extension requires repeating authorization checks
Comprehensive Audit Trail for Each Reveal
Given any reveal attempt (granted or denied) When the attempt occurs Then an immutable audit record is created capturing user ID, role, community, item ID/type, sensitivity label, timestamps (start/end), reason (if provided), outcome, policy references consulted, IP/device, and authentication freshness result Given an auditor with audit permissions When they query the audit log interface by date range, user, community, item, or outcome Then matching records are returned accurately and consistently with what occurred
Reason Capture & Policy Enforcement
"As a board member, I want to provide a required, specific reason before revealing an original so that the access is justified and defensible during audits."
Description

Require a structured justification for every reveal, including an admin-configurable reason picklist, minimum free-text detail, and an optional link to a violation/case ID. Validate content sensitivity against policy (e.g., PII level) and block vague reasons with inline guidance. Persist the reason with the reveal record, surface it in the session header and watermark metadata, and make it filterable in audits. This enforces purpose limitation and supports consistent, audit-ready documentation across communities.

Acceptance Criteria
Gate Reveal on Structured Reason Entry
Given an authorized reviewer clicks Reveal on a masked item When the Reason modal is displayed Then the Reason Category picklist is required And the Reason Details field is required with a minimum of 20 non-whitespace characters And the Case/Violation ID field is optional and accepts either an ID pattern ([A-Z]{2,10}-\d{3,8}) or a valid https URL And the Confirm Reveal action remains disabled until all required validations pass And upon confirmation, the reason inputs are captured to the reveal record with timestamp and user ID
Admin-Configurable Reason Picklist
Given a community admin configures the Reason Category picklist for Community A with options, order, and flags (e.g., Requires Case ID) When a reviewer opens the Reason modal in Community A Then the picklist reflects the configured options and order And deactivated options are not shown And any option flagged as Requires Case ID enforces the Case/Violation ID field as required with validation
Vague Reason Blocking with Inline Guidance
Given the reviewer is entering text in Reason Details When the text is fewer than 20 non-whitespace characters or matches a banned term list ["test","n/a","na","none","asdf","lorem","—","-"] Then an inline validation message appears: Please provide a specific investigative purpose (20+ characters). Avoid vague entries like 'test' or 'n/a'. And the Confirm Reveal action remains disabled And when the text meets the threshold and does not match a banned term, the validation clears without page reload
Policy-Based Sensitivity Enforcement
Given the item to be revealed has sensitivity metadata PII Level = High And the community policy allows reveal only for Reason Categories ["Legal Request","Fraud/Abuse Investigation","Regulatory Inquiry"] at PII=High When the reviewer selects a disallowed category or lacks the required reviewer permission per policy Then the reveal is blocked with inline guidance referencing the policy name and allowed categories And a policy_violation event is logged with attempted category and sensitivity And when the reviewer selects an allowed category and holds required permission, the reveal can proceed subject to other validations
Reason Visibility in Session Header and Watermark Metadata
Given a reveal is confirmed When the unmasked content is displayed Then the session header shows Reason Category, truncated Reason Details (first 80 chars), and Case/Violation ID if provided And the visible watermark reads For Review on all pages/views And watermark metadata attached to the session includes fields: reasonCategory, reasonDetails, caseIdOrLink, revealedBy, revealedAt And exporting the session report exposes these metadata fields
Reveal Record Persistence, Immutability, and Expiry Remasking
Given a reveal is confirmed Then a reveal record is created storing userId, timestamp, itemId, reasonCategory, reasonDetails, caseIdOrLink, sensitivityLevel, and policyVersion And the stored reason fields are immutable to the reviewer after creation And when the configured reveal duration elapses, the item is automatically re-masked without user action And the reveal record remains accessible in audit logs after re-masking
Audit Filtering and Export by Reason
Given multiple reveal records exist with varied categories, texts, and case IDs When an auditor filters by Reason Category, Reason Details contains substring, Case/Violation ID present/absent, date range, and reviewer Then the audit results return only matching records within 2 seconds for up to 10,000 records And results can be exported to CSV including fields reasonCategory, reasonDetails, caseIdOrLink, sensitivityLevel, policyOutcome And opening a record shows the captured reason values and a link to the associated item
Watermarked For‑Review Overlay
"As a treasurer, I want revealed documents watermarked with my identity and the expiry so that any shared copy is traceable and clearly marked as temporary review material."
Description

Render a non-removable, dynamic “For Review” watermark across all revealed content (images, PDFs, text/html views) that includes viewer name, role, timestamp, reason, session ID, and expiry time. Disable printing and downloading where technically feasible; when downloads are permitted by policy, embed the same watermark. Ensure responsive, high-DPI rendering for web and mobile, and preserve legibility without obscuring critical content. This deters leakage, enables attribution, and provides clear context that access is temporary and restricted.

Acceptance Criteria
Web Image Reveal Overlay
Given an authorized reviewer completes the Timed Reveal gate for an image asset and provides a reason When the image is displayed in the viewer Then a non-removable For Review watermark overlay is visible across the entire image and includes viewer full name, role, ISO 8601 timestamp with timezone, reason, session ID, and expiry time And the overlay persists during zoom, pan, or scroll and cannot be hidden or removed via client-side DOM/CSS changes And attempts to open the image source directly are denied unless policy permits, in which case the served image is identically watermarked
PDF Viewer Overlay and Print Controls
Given an authorized reviewer reveals a multi-page PDF with a required reason When navigating between pages, zooming, or switching view modes Then each page renders with the same For Review watermark overlay and metadata and it remains present on every page And printing via browser/OS is blocked if policy disallows; if policy permits printing, printed output for every page contains the same watermark And download controls are hidden/disabled unless policy permits
HTML/Text Reveal Responsive High-DPI Rendering
Given an authorized reviewer reveals an HTML/text document on web or mobile with a required reason When the viewport varies between 320px and 1920px width and device pixel ratio is 1.0, 2.0, or 3.0 Then the watermark renders crisply at all sizes and densities and scales responsively without pixelation And interactive controls remain clickable (overlay does not capture pointer events) and the overlay never obscures fixed UI controls And at 100% zoom the underlying text remains readable with the overlay applied And on orientation change the overlay reflows within 500 ms without flicker
Watermark Metadata Accuracy and Live Updates
Given a reveal is approved and started When the overlay is drawn Then it displays the authenticated viewer full name and role, the reveal start timestamp in ISO 8601 with timezone, the provided reason as sanitized plain text, the session ID, and the scheduled expiry time And if the session expiry is extended or shortened while the viewer is open, the displayed expiry updates within 2 seconds And if the reason contains HTML/script, it is rendered as plain text with dangerous markup stripped
Print and Download Enforcement
Given a revealed item is being viewed under a policy that disallows print and download When the user attempts to print via menu, keyboard shortcut, or window.print() Then printing is blocked and the attempt is logged with timestamp and session ID And when the user attempts to download or save via UI controls, context menu, or direct asset URL, the original asset is not served and access is denied/logged And when policy allows print/download, any printed or downloaded output includes the identical watermark data
Watermark Embedded in Permitted Downloads
Given policy permits download for the revealed item When the user downloads a PDF, image, or text/HTML export Then the file includes a non-removable For Review watermark with the same metadata on every PDF page, burned into image pixels, or inserted as header/footer in text/HTML exports And the embedded watermark persists when opened in third-party viewers/editors or after re-upload
Legibility Without Obscuring Critical Content
Given standard test corpus documents (images with faces/maps, PDFs with body text 12–14px, HTML pages with forms) When viewed at 100% zoom on desktop and at 375x667 DPR2 on mobile Then the overlay does not obscure more than 20% of any single line of body text or more than 30% of any primary subject region in images, and all mandatory labels remain readable And no overlay element overlaps fixed UI controls (e.g., navigation, close, print/download buttons)
Time-Bound Access & Auto Re‑Mask
"As a part-time manager, I want reveal sessions to expire automatically and re-mask content so that sensitive information is not left exposed after my review."
Description

Support admin-configurable default and maximum reveal durations (e.g., 5/15/60 minutes) with an on-screen countdown and optional extension requests. On expiry, immediately revoke the session, invalidate any pre-signed links, and re-mask the content across all open clients. Enforce server-side checks on each content fetch to prevent stale access. Emit on-start and on-expiry events to power notifications and downstream automations. This minimizes persistent exposure while keeping investigations efficient.

Acceptance Criteria
Enforce Default and Maximum Reveal Durations
Given admin configuration defaultDuration=15m and maxDuration=60m When a reviewer initiates a timed reveal without specifying a duration Then the session expiry is set to now+15m and the UI countdown starts at 15:00 Given admin configuration defaultDuration=15m and maxDuration=60m When a reviewer requests a 75m duration Then the session duration is capped at 60m and the UI displays a non-blocking notice "Capped to 60 minutes" Given an active reveal session created at T0 with duration D When the service restarts or the client refreshes Then the server persists and returns the original expiresAt and the client resumes an accurate countdown within ±1s drift Given a new reveal session When the client calls GET /reveal/:id Then the response includes expiresAt (ISO-8601 UTC) and remainingSeconds >= 0 consistent with server time
Accurate On-Screen Countdown Timer
Given an active reveal session with expiresAt When the countdown is rendered Then remaining time is computed from server time (not local clock) and updates at 1s intervals with ≤1s drift Given remainingSeconds <= 60 When the countdown updates Then the UI shows a prominent expiry warning state (color change + label) without blocking interaction Given the client loses network connectivity during an active session When connectivity is restored Then the countdown re-synchronizes to server time on first successful ping and corrects any drift within 1s Given the same user has multiple tabs/windows open to the same reveal When time elapses Then all tabs display consistent remaining time and transition to expired within 2s of each other
Extension Requests Within Policy Limits
Given extensionsEnabled=true, maxDuration=60m, and an active 15m session When the reviewer requests a +10m extension 5m before expiry Then expiresAt is extended by 10m and the countdown updates immediately Given an active session with elapsed=20m and maxDuration=60m When the reviewer requests +50m Then the extension is capped so total duration does not exceed 60m and a notice indicates "Capped to maximum" Given an active session When the reviewer submits an extension request after expiry Then the request is rejected with HTTP 410 and the content remains masked Given extensionsEnabled=false When the reviewer attempts to request an extension via UI or API Then the UI affordance is hidden and the API responds 403 FORBIDDEN Given any approved extension When the extension is applied Then an audit record is written with sessionId, userId, previousExpiry, newExpiry, and reason (if provided)
Immediate Auto Re‑Mask on Expiry
Given an active timed reveal session with expiresAt=T When the current server time reaches T Then within 2s all open clients replace revealed content with its masked placeholder and show "Access expired" Given any attempt to scroll, zoom, print, or download the revealed asset after expiry When the request is made Then the action is blocked and no new pixels or bytes of the original are rendered or transmitted Given a streaming or paginated asset is mid-load When expiry occurs Then any in-flight transfers are aborted and partial renders are re-masked
Invalidate Pre‑Signed Links and Cached URLs on Expiry
Given asset delivery uses pre-signed URLs scoped to the reveal session When the session expires Then subsequent GET (including Range/conditional requests) to those URLs return HTTP 403 with error code REVEAL_EXPIRED Given CDN caching is configured When a pre-signed URL expires Then cache-control prevents serving stale content (no-store) and origin returns 403 on revalidation Given a user retains a previously issued pre-signed URL When they attempt to reuse it after expiry Then the request is denied regardless of client cache and no content bytes are returned
Server-Side Expiry Check on Every Fetch
Given any content fetch request (bytes, thumbnails, pages) includes a session token When the server processes the request Then it validates authorization and expiry server-side and rejects expired sessions with HTTP 403 without leaking content length Given a request arrives within the final second before expiry When the server evaluates expiresAt Then the decision is based solely on server time; requests with receivedAt >= expiresAt are rejected Given parallel requests for the same asset fragment When one completes after expiry Then the server aborts processing and returns 403 without completing the response body
On‑Start and On‑Expiry Events Emitted
Given a reveal session is started When the session is created Then an event reveal.started is emitted within 1s containing tenantId, userId, contentId, sessionId, expiresAt Given a reveal session ends due to timeout or manual revoke When expiry or revoke occurs Then an event reveal.expired is emitted within 1s with cause, sessionId, and lastSeenAt Given transient delivery failure to the webhook endpoint When an event cannot be delivered Then the system retries with exponential backoff for at least 24h and exposes delivery status for monitoring Given duplicate delivery attempts When the consumer receives the same eventId more than once Then the event body is identical and marked as a retry to support idempotent processing
Immutable Audit Logging & Export
"As an auditor, I want a tamper-evident log of every reveal with who, what, when, and why so that I can verify proper access and investigate anomalies."
Description

Capture a complete, append-only record of each reveal lifecycle event—request, approval (if applicable), start, end/expiry, viewer identity, reason, IP/device, content identifiers (hashes), and restricted actions (e.g., print/download attempts). Store logs in a tamper-evident format (hash-chained) with configurable retention policies per community. Provide an admin UI and export (CSV/JSON) with filters by user, content, time range, community, and reason. This enables audit readiness, incident response, and compliance reporting.

Acceptance Criteria
Lifecycle Event Logging Completeness
Given a reveal request with required reason and target content is submitted, When the request is created, Then an append-only audit event is written with fields: event_type=requested, request_id, requester_user_id, community_id, content_id, content_hash, reason, timestamp (ISO 8601 UTC), requester_ip, requester_device_fingerprint, and sequence_number. Given the community requires approval, When the request is approved or denied, Then an audit event is written with event_type=approved or denied, approver_user_id, decision_reason (optional), timestamp, approver_ip, approver_device_fingerprint, and sequence_number > previous. Given an approved reveal, When the reviewer starts viewing, Then an audit event is written with event_type=reveal_started, viewer_user_id, request_id, timestamp, viewer_ip, viewer_device_fingerprint, active_watermark=true, and sequence_number > previous. Given an active reveal, When the reveal ends due to expiry, Then an audit event is written with event_type=reveal_ended, end_reason=expired, end_timestamp, auto_remask=true, and sequence_number > previous. Given an active reveal, When the reviewer manually ends the reveal, Then an audit event is written with event_type=reveal_ended, end_reason=manual, end_timestamp, auto_remask=true, and sequence_number > previous. Given restricted actions are attempted during a reveal, When the viewer attempts print or download or copy, Then an audit event is written with event_type=restricted_action_attempt, action=print|download|copy, result=blocked|allowed, timestamp, viewer_user_id, and sequence_number > previous. Given any audit event is written, When it is persisted, Then it is immutable (no update/delete permitted) and is retrievable by request_id and community_id.
Tamper-Evident Hash Chain Integrity Verification
Given a new audit event is appended for a community, When it is stored, Then record_hash = SHA-256(serialized_payload || prev_hash) and prev_hash equals the prior event's record_hash within the same community chain. Given the audit log chain for a community, When integrity verification runs, Then any modification to an event's payload or order causes verification to fail with the first offending event identified by request_id and sequence_number. Given an admin runs integrity verification via API/UI, When the check completes, Then the result includes: community_id, checked_range (start_sequence..end_sequence), status=PASS|FAIL, verification_timestamp (UTC), and current_tail_hash. Given the append endpoint, When a request attempts to insert an event with sequence_number <= current_tail or missing prev_hash, Then the write is rejected with HTTP 409 Conflict and no event is appended. Given storage or process errors, When an event cannot be written atomically with its hash, Then the operation is aborted and no partial event is visible in queries.
Per-Community Retention Policy Configuration and Enforcement
Given a system admin sets a community's audit retention to 365 days, When the setting is saved, Then a configuration audit event is recorded with old_value, new_value, admin_user_id, and timestamp. Given a community with retention=30 days and events older than 31 days, When the nightly purge job runs, Then events strictly older than 30 days are no longer retrievable via UI or API and a retention_purge audit event summarizing purged range is appended. Given a purge has removed older events, When chain integrity is verified on remaining events, Then verification passes starting from the new chain anchor created at the first retained event. Given retention is increased (e.g., 30 to 180 days), When saved, Then no previously purged events reappear and future purges honor the new window. Given retention is decreased (e.g., 365 to 90 days), When the next purge runs, Then events older than 90 days are purged and the purge action is logged. Given a user queries a time range that includes purged data, When results are returned, Then counts and summaries exclude purged events and the UI surfaces a banner indicating retention may limit results.
Admin UI Filtering, Authorization, and Performance
Given an authenticated user without Audit Admin permission, When they navigate to the audit log UI, Then access is denied with 403 and no data is leaked. Given an Audit Admin on the audit log UI, When they filter by user, content_id, time range (UTC), community, and reason, Then results include only events matching all filter predicates (AND semantics) and show total count and page count. Given a valid filter returning 50,000 events, When the first page (up to 100 rows) is requested, Then server response time is ≤ 2 seconds at p95 and rows are correctly sorted by timestamp desc by default. Given the admin changes sort to ascending by timestamp and adds a reason filter, When results load, Then ordering and filtering update correctly and persist across pagination. Given an event row is opened, When details are viewed, Then all captured fields are displayed including IP and device fingerprint, with timestamps in the viewer's preferred timezone and a toggle to view raw UTC. Given the UI is used to view logs, When a page is viewed or an event is opened, Then a view_audit event is recorded including viewer_user_id, filters applied, and timestamp.
Filtered Export to CSV and JSON
Given an Audit Admin has applied filters, When they request an export as CSV, Then the exported file contains only matching events, includes a header row, uses UTF-8 encoding, comma delimiter, RFC 4180 quoting, and timestamps in ISO 8601 UTC. Given an Audit Admin has applied filters, When they request an export as JSON, Then the file is emitted as NDJSON (one JSON object per line) with the same fields as CSV and timestamps in ISO 8601 UTC. Given an export produces N events, When the download completes, Then the file metadata includes record_count=N and sha256_checksum matching the file content. Given an export would exceed 1,000,000 records, When the export is requested, Then the system produces multiple chunked files each ≤ 1,000,000 records and a manifest.json listing filenames, counts, and checksums. Given any export is generated, When it starts and completes (or fails), Then corresponding audit events (export_started, export_completed|export_failed) are recorded including filter summary, format, counts, and requester_user_id. Given an export link is generated, When a non-authorized user attempts to download it, Then access is denied with 403 and the attempt is audited.
Access Control and Append-Only Protections
Given any API or UI endpoint for audit logs, When a client attempts to update or delete an existing event, Then the request is rejected with 405 Method Not Allowed or 403 Forbidden and no change occurs. Given an authorized service attempts to append a backdated event, When the prev_hash and sequence_number do not match the current tail, Then the append is rejected and the attempt is audited. Given a non-admin user attempts to view audit logs via API, When the request is made, Then the response is 403 and no row-level or aggregate metadata is returned. Given an Audit Admin views logs, When they access any event details, Then that read access is itself recorded as a view_audit event with viewer_user_id, event_id, and timestamp. Given system roles are configured, When permissions are evaluated, Then only users with Audit Admin or Compliance Reviewer roles can access the audit UI and export endpoints.
Admin Policy Configuration
"As a community admin, I want to define reveal policies and templates so that access is consistent, compliant, and tailored to our community’s risk tolerance."
Description

Offer a centralized policy console for community admins to configure who can reveal (roles/scopes), allowed content types, required reason templates, default/max duration, download/print restrictions, watermark style, and whether approvals are required for high-sensitivity items. Support community-level defaults with overrides, versioned policy changes with effective dates, and localization of reason templates. Integrates with Duesly’s roles and community settings for consistent governance.

Acceptance Criteria
Reveal Eligibility by Role/Scope
Given a community policy allows revealer roles BoardMember and ComplianceReviewer When a user with the BoardMember role initiates a Timed Reveal Then the reveal is permitted and the audit log records user ID, role/scope, timestamp, and policy version Given the same policy When a user without an allowed role attempts a Timed Reveal Then the action is blocked with a 403 error and message "Reveal not permitted by policy", and the attempt is logged with user ID, attempted content, timestamp, and policy version Given an admin updates the policy to include PropertyManager and publishes it When a user with PropertyManager attempts a reveal after the update Then the permission change takes effect within 60 seconds of publish without requiring user re-authentication
Allowed Content Types Enforcement
Given policy Allowed Content Types = [Announcement, PaymentReceipt]; Disallowed = [PrivateDocument] When a user initiates a Timed Reveal for a PrivateDocument Then the system blocks the action with message "Content type disallowed by policy" and logs the denial with content ID, type, user ID, and policy version Given the same policy When a user initiates a Timed Reveal for an Announcement Then the reveal proceeds and the audit log records the allowed content type and policy version Given an admin edits Allowed Content Types to include PrivateDocument effective immediately When a user attempts a reveal for a PrivateDocument after the change Then the reveal is permitted and logged under the new policy version
Required Reason Templates and Localization
Given an admin creates reason templates with keys and localized labels: { key: INVESTIGATION, en-US: "Investigation", es-ES: "Investigación" } and marks reasons as Required When a reviewer with preferred locale es-ES initiates a Timed Reveal Then the reason selector displays labels in Spanish and selection is required before continuing And the selected reason key and rendered label are stored in the audit log along with any optional note Given a reviewer with locale fr-FR and no fr-FR translation exists When they initiate a Timed Reveal Then the reason labels fall back to the community default locale en-US and selection is still required Given a reason template has "Require note" enabled When a reviewer selects that reason Then the Confirm action remains disabled until a non-empty note is entered and both reason key and note are persisted
Default and Maximum Reveal Duration Limits
Given policy sets Default Duration = 15 minutes and Max Duration = 60 minutes When a reviewer opens the Timed Reveal dialog Then the duration field is pre-filled to 15 minutes and cannot be set above 60 minutes via UI or API Given a reviewer attempts to set duration to 90 minutes via API When the request is submitted Then the system rejects it with 400 "Duration exceeds policy maximum" and no reveal is created Given an admin updates Default Duration to 30 minutes (Max remains 60) When a new reveal is initiated after the update Then the default presented is 30 minutes And any active reveals created before the update keep their original expiry time
Content Handling Restrictions and Watermark Enforcement
Given policy sets Download = Disabled, Print = Disabled, Watermark = { text: "For Review", opacity: 30%, pattern: Diagonal } When a reveal session starts Then download and print controls are not shown, download/print endpoints return 403 if invoked, and the content is rendered with the configured watermark plus reviewer name and timestamp Given policy sets Download = Enabled and Print = Disabled When a reviewer downloads during a reveal Then the downloaded file contains an embedded watermark with the configured style and request metadata, and the print action remains blocked Given a reviewer attempts to remove or hide the watermark via client controls When the content is re-rendered or refreshed Then the watermark remains enforced and cannot be disabled by the reviewer
Approval Workflow for High-Sensitivity Items
Given policy requires Approval = Required for sensitivity = High and Approval Roles = [ComplianceOfficer] When a reviewer requests a Timed Reveal for a High-sensitivity item Then the request moves to Pending Approval, no content is revealed, and approvers receive an in-app notification Given an approver approves the request and selects a duration within the policy max When the approval is submitted Then the reveal is activated for the approved duration, the requester is notified, and the audit log captures approver ID, decision, timestamp, and policy version Given an approver rejects the request When the rejection is submitted Then the request is closed with message "Request denied" and no reveal is activated Given a Medium-sensitivity item under the same policy When a reviewer requests a reveal Then the reveal proceeds immediately without approval
Versioned Policies and Community Defaults with Overrides
Given a Community Default policy v1 is active now and an override policy v2 for content type PrivateDocument is scheduled with an effective date in the future When a reviewer initiates a reveal for a PrivateDocument before the v2 effective date Then v1 rules are applied and the audit log records policy version v1 When the v2 effective date/time is reached And a reviewer initiates a reveal for a PrivateDocument Then v2 rules are applied and the audit log records policy version v2 Given a reveal was created under v1 and is still active when v2 becomes effective When the reveal continues past the effective time Then the reveal retains v1 parameters (non-retroactive) until it expires Given an admin edits any policy When the change is saved Then a new immutable version is created with editor ID, timestamp, diff summary, and effective date, visible in the policy version history
Approval Workflow & Notifications
"As a board president, I want certain reveals to require approval and notify stakeholders so that sensitive access is governed and transparent."
Description

Provide an optional, policy-driven approval step for reveals that meet high-risk criteria (e.g., sensitive PII, long durations). Route requests to designated approvers with in-app and email notifications containing reason, content summary, and requested duration. Support single or two‑person approval, SLAs with auto-expiry, and a clear audit trail of decisions. Notify requesters and admins on approval/denial, reveal start, and reveal expiry. This adds governance without slowing routine reviews.

Acceptance Criteria
High-Risk Reveal Requires Approval Gate
Given a reveal request matches any policy-defined high-risk rule (e.g., Sensitive PII tag present or requested duration exceeds threshold) When the requester submits the request with a required reason Then the system routes the request to approvers, sets status to "Pending Approval", blocks access to the original until approval, displays a pending state to the requester, and writes an audit entry with matched rule(s) And Given a reveal request that does not match any high-risk rule When submitted Then the system bypasses approval and initiates the Timed Reveal per standard flow without approver routing And Given a reveal request without a reason When the requester attempts to submit Then submission is prevented and a validation error is shown
Approver Routing and Notification Payload
Given a pending approval request When routing occurs Then the system selects designated approver(s) based on policy mapping (community, role, content class) excluding the requester, and if no approver is found, routes to the configured admin fallback And When notifications are sent Then each approver receives both in-app and email notifications containing: requester name, content summary, reason provided, requested duration window, policy rule that triggered approval, SLA deadline timestamp in approver's timezone, and a deep link to Approve or Deny And Then the system records delivery attempts and outcomes for each channel in the audit log
Single-Approver Decision and Effects
Given a policy that requires a single approver When any designated approver approves Then the request transitions to "Approved", the approver identity and optional comment are recorded, the requester and admins are notified, and the Timed Reveal is scheduled/started per the requested window with a "reveal start" notification at the moment access becomes available And When any approver denies Then the request transitions to "Denied", a denial reason is required and recorded, requester and admins are notified, no reveal occurs, and the audit log is updated And Then approver actions are idempotent; repeated clicks do not change the final decision
Two-Person Approval Enforcement
Given a policy that requires two approvals When the first approver approves Then the request transitions to "Partially Approved" and remains blocked And When a second, distinct approver approves within the SLA Then the request transitions to "Approved" and proceeds as configured And When any approver denies at any time Then the request immediately transitions to "Denied" and no further approvals are accepted And Then the requester cannot approve their own request, and the same approver cannot count twice
SLA and Auto-Expiry of Pending Requests
Given an approval SLA is configured (e.g., 48 hours) When the SLA elapses without a final approval decision Then the request transitions to "Expired", all approval links are disabled, no reveal occurs, and requester and admins are notified of the expiry And If the request is "Partially Approved" at SLA expiry Then it also transitions to "Expired" with the same outcomes And Then the audit log captures the SLA deadline, expiry time, and reason "Auto-expired per SLA"
Lifecycle Notifications to Requester and Admins
Given a request transitions to Approved or Denied When the decision is recorded Then the requester and admins receive in-app and email notifications containing the decision, approver(s) identity, any approver comments, and the policy rule basis And Given an Approved request When the reveal actually starts Then the requester and admins are notified of reveal start time and end time And Given an active reveal When the reveal expires and is re-masked Then the requester and admins are notified of expiry and re-mask confirmation
Audit Trail & Traceability
Given any approval workflow event When it occurs Then the system writes an immutable audit record including correlation ID, timestamp, actor, action (submit, route, notify, approve, deny, expire), notification channel and delivery status (for notifications), matched policy rule(s), requested duration, final approved duration, and decision notes And Then authorized admins can view the full decision history for a request in the audit log UI, with filters by community, requester, approver, status, and date range

Parcel Guard

Auto‑crops photos to your property boundary using GPS and parcel maps, trimming backgrounds that often include neighbors, kids, or unrelated houses. Clear guidance during capture helps frame compliant shots, resulting in cleaner evidence and fewer neighbor concerns.

Requirements

Parcel Boundary Map Integration
"As a community manager, I want the app to know my property’s exact boundary so that captured photos are automatically constrained to the correct parcel."
Description

Fetch and cache parcel polygons from authoritative GIS sources, align them to the property associated with the compliance item, and reconcile with device GPS to build a precise crop mask. Support geocoding from the unit’s address in Duesly, handle multi-parcel properties, and apply an accuracy threshold with fallback prompts when location precision is low. Expose a service that returns the boundary mask and metadata to the capture and post-processing pipelines, ensuring consistent behavior across iOS, Android, and web upload flows.

Acceptance Criteria
Authoritative GIS Fetch and Caching
Given a property in Duesly with a valid address and no cached parcel polygons When the boundary service is invoked Then it fetches polygon(s) from the configured authoritative GIS source(s), validates geometry (closed, non-self-intersecting), and stores the result in cache with a configurable TTL. Given parcel polygons are present in cache and not expired When the boundary service is invoked Then it returns the cached polygon(s) with metadata cacheHit=true and cacheAgeMs, and makes no external GIS requests. Given the primary GIS source fails (timeout or 4xx/5xx) When the boundary service is invoked Then it attempts the configured secondary source(s); if all sources fail, it returns HTTP 404 with errorCode=PARCEL_POLYGON_NOT_FOUND and no mask.
Address Geocoding to Parcel Match (Web Upload / No GPS)
Given a Duesly property has a validated postal address and the client provides no GPS data When the boundary service is invoked Then it geocodes the address and matches parcel polygon(s) overlapping the geocoded point, returning a mask and metadata including geocodingConfidence in [0,1]. Given geocodingConfidence is below the configured threshold When the boundary service is invoked Then it returns no mask and sets action=REQUEST_USER_PIN with reason=LOW_CONFIDENCE.
GPS Reconciliation and Accuracy Threshold (Mobile)
Given the client supplies device coordinates with horizontalAccuracy in meters When the boundary service reconciles GPS with parcel polygon(s) Then if horizontalAccuracy <= configuredAccuracyM and the point lies within the polygon or within bufferM, it sets locationStatus=trusted and returns the mask. Given horizontalAccuracy > configuredAccuracyM or the point lies outside the polygon beyond bufferM When the boundary service is invoked Then it returns no mask and sets action=REQUEST_MOVE_CLOSER or action=REQUEST_USER_PIN with reason=LOW_GPS_ACCURACY.
Multi‑Parcel Property Union Mask
Given the property maps to multiple parcel IDs When the boundary service composes the crop mask Then it returns a single unioned Polygon/MultiPolygon with topology preserved, and metadata includes parcelIds[] and unioned=true. Given parcels are discontiguous beyond the configured separation threshold When the mask is generated Then metadata includes warning=DISCONTIGUOUS_PARCELS and separationMeters.
Boundary Mask Service Contract and Metadata
Given any client requests a boundary mask When the service responds Then the JSON payload conforms to the published schema containing: schemaVersion, propertyId, parcelIds[], geometry (GeoJSON Polygon or MultiPolygon, CRS EPSG:4326), bbox, source, cacheHit, cacheAgeMs, geocodingConfidence (optional), gpsAccuracyM (optional), timestamp (ISO 8601 UTC), unioned (optional), warning (optional), errorCode (on failure), action (on fallback). Given the service responses are validated in automated contract tests When validated against the published JSON schema Then 100% of responses pass schema validation across iOS, Android, and web clients.
Cross‑Platform Consistency
Given identical property inputs and cache state When iOS, Android, and web request the boundary mask within the same cache window Then geometry coordinates and parcelIds[] are identical across platforms, and schemaVersion matches. Given timestamps are included in responses When parsed on each platform Then they are formatted as ISO 8601 UTC and parse without locale-dependent differences.
Fallback Prompt Flow and Manual Pin Recovery
Given the service cannot produce a mask due to low GPS accuracy or low geocoding confidence When the boundary service is invoked Then it returns no mask and includes action with a guidanceKey (e.g., REQUEST_USER_PIN or REQUEST_MOVE_CLOSER) and a machine-readable reason. Given the user provides a manual pin inside the approximate property boundary When the client resubmits with pin coordinates Then the service returns a mask with locationStatus=manual and metadata includes pinLocation and previousAction.
Real-time Capture Guidance
"As a resident submitting evidence, I want real-time guidance while taking a photo so that my shot is compliant without trial and error."
Description

Provide an in-camera overlay of the parcel outline, live framing tips, and distance/orientation cues to help users compose compliant shots. Include stability detection, low-light warnings, horizon leveling, and haptic feedback when framing is acceptable. Display privacy hints (e.g., avoid neighbor spaces) and block capture when the user is outside the parcel geofence if configured. Localize guidance text and ensure accessibility with high-contrast and screen reader support.

Acceptance Criteria
In-Camera Parcel Overlay Accuracy and Responsiveness
Given the user is inside the parcel geofence and the parcel polygon is available When the camera preview is opened Then the parcel outline renders within ≤3 m mean edge deviation from the authoritative parcel map and appears within ≤500 ms Given the user pans/tilts the device by ≥15° When overlay redraw occurs Then the overlay updates at ≥10 Hz with input-to-render latency ≤100 ms and the preview maintains ≥24 FPS Given location accuracy degrades When reported horizontal accuracy is >15 m for ≥3 s Then show a "Location weak" banner and switch the outline to a dashed style until accuracy ≤15 m
Framing Tips, Orientation Cues, and Haptic Confirmation
Given the parcel outline is visible When more than 10% of the live view extends beyond the parcel boundary Then display directional framing tips (e.g., Move left/right/back) until outside area drops to ≤5% Given device roll is outside ±2° When roll >2° is detected for ≥300 ms Then show a horizon level indicator and an arrow to rotate; when roll returns to ±2°, show a "Level" state Given distance/orientation cues are enabled When subject distance is estimated <1.5 m or >15 m (via depth/focus estimation) or yaw offset from parcel normal >10° Then display "Move closer/back" or "Rotate" respectively until within 2–10 m and yaw ±5° Given framing is acceptable (outside area ≤5%, roll within ±2°, yaw within ±5°) When these conditions hold for ≥500 ms Then emit a distinct haptic double-tap and display "Framing OK"
Stability Detection and Steady‑Shot Gate
Given the camera preview is active When gyroscope RMS angular velocity over 500 ms exceeds 3°/s or accelerometer RMS >0.02 g Then show a "Hold steady" tip overlay Given "Steady‑shot required" is enabled When stability improves to <1.5°/s RMS for ≥500 ms Then enable the shutter Steady state and allow capture; otherwise, block capture and provide a single short vibration on shutter press Given horizon leveling is enabled When a capture occurs Then the captured frame roll deviation from level is ≤2°
Low‑Light Detection and Guidance
Given the preview metering reports target exposure slower than 1/30 s at max allowed ISO or EV100 < 6 When the preview is active Then show a Low‑light banner within ≤200 ms and suggest flash or stabilization Given flash is available and enabled When the shutter is pressed Then flash fires and target exposure time is ≤1/60 s Given low‑light persists and Steady‑shot is required When the shutter is pressed without flash Then block capture unless motion stability is <1.0°/s RMS for ≥800 ms
Privacy Hints to Avoid Neighbor Spaces
Given computer vision detects faces/people or vehicles with confidence ≥0.8 outside the parcel boundary When these are present in the live view Then display a privacy hint ("Avoid neighbor spaces") and softly mask non‑parcel regions Given >10% of the live view lies outside the parcel boundary When this persists for ≥2 s Then keep the privacy hint visible; when reduced to ≤5%, dismiss the hint within 500 ms Given privacy hints are enabled When hints are shown Then they do not obstruct the parcel outline and remain readable with contrast ≥4.5:1
Geofence‑Based Capture Blocking
Given "Block outside parcel" is enabled When the user’s estimated position (considering horizontal accuracy radius) is outside the parcel polygon for ≥2 s Then disable the shutter, show "Inside parcel required to capture", and provide a single short vibration on shutter press Given the user re‑enters the parcel or accuracy improves such that the accuracy radius intersects the parcel When this condition holds for ≥1 s Then re‑enable the shutter and remove the banner Given a blocked capture attempt When it occurs Then no image is saved and an audit log entry is recorded with timestamp and reason "Outside geofence"
Localization and Accessibility Compliance
Given the device language is set to any supported locale When the camera guidance UI loads Then all guidance strings are localized with no truncation/overlap on 4.7–6.7 inch screens; missing keys fall back to English Given high‑contrast mode is enabled When guidance UI renders Then all text and key overlay elements meet WCAG 2.1 AA contrast (text ≥4.5:1, non‑text ≥3:1) and non‑color cues (haptic/shape) accompany color changes Given a screen reader (VoiceOver or TalkBack) is active When guidance elements are focused or updated Then each has an accessible name and hint, the parcel outline is announced as "Parcel boundary outline", and dynamic hints are announced no more than once per 5 s
Auto Crop & Privacy Masking
"As a board member, I want photos to auto-crop to our parcel and mask sensitive content so that evidence is clear and respects neighbors’ privacy."
Description

After capture, automatically crop the image to the parcel boundary and apply privacy masks to faces, license plates, and any content detected outside the boundary. Perform processing on-device when possible to reduce latency and preserve privacy; fall back to server-side processing when needed. Provide configurable mask strength, edge feathering, and an allowlist for HOA-owned shared areas. Ensure output meets evidence standards while minimizing inclusion of neighboring properties or bystanders.

Acceptance Criteria
On-Device Processing With Server Fallback
Given a supported device with available on-device model and sufficient resources, When a 12MP photo is captured, Then auto-crop and privacy masking are executed on-device and complete in ≤2s and no image bytes are uploaded. Given the on-device model is unavailable or resource-constrained, When the photo is captured, Then the image is uploaded over TLS 1.2+ for server processing and the end-to-end processing completes in ≤5s at p95. Given the device is offline and server processing is required, When the photo is captured, Then the job is queued locally with at-least-once retry, the user is notified of pending processing, and processing completes within 60s of reconnect at p95.
Parcel Boundary Auto-Crop Fidelity
Given GPS accuracy ≤10m and a valid parcel polygon for the property, When the photo is processed, Then no pixels outside the parcel polygon are visible in the final output (crop or mask outside), and parcel edge alignment error is ≤2% of parcel perimeter length. Given edge feathering is configured, When the final crop/mask is rendered, Then feathering is applied uniformly along the boundary at the configured radius (0–20px), with ≤1px visible seams. Given the parcel polygon cannot be fully contained within a rectangular crop, When processing, Then pixels outside the polygon are masked with the configured style rather than shown.
Privacy Masking Coverage for Faces and Plates
Given faces and license plates are present outside the parcel boundary, When processing runs, Then all detected faces and plates outside-boundary are masked using the configured style and strength, and masks extend ≥10px beyond detection boxes. Given a validation set of ≥500 representative images, When evaluated, Then face/plate masking recall ≥0.98 and precision ≥0.95 for outside-boundary detections. Given any pixels lie outside the parcel and are not within an allowlisted shared-area polygon, When rendering the final image, Then those pixels are masked and are not human-recognizable.
Configurable Mask Strength and Edge Feathering
Given community-level settings for mask type (blur, pixelate, solid) and strength, When an image is processed, Then the selected mask type is applied with strength within allowed ranges (blur σ 4–20, pixelate block 8–40px, solid opacity 60–100%). Given edge feathering is configured 0–20px, When masks/crop boundaries are rendered, Then feathering equals the configured value ±1px. Given an admin updates masking settings, When the next photo is processed, Then new settings are applied without app restart and are recorded in the processing metadata.
HOA Allowlist for Shared Areas
Given HOA-owned shared areas are defined as polygons and allowlisted, When processing an image, Then pixels within allowlisted polygons are exempt from privacy masking even if outside the parcel boundary. Given allowlist data changes on the server, When a device syncs, Then updated allowlist polygons are applied within 1 minute and logged with version and timestamp. Given an allowlist entry is removed, When subsequent images are processed, Then masking is re-applied to those regions and the change is reflected in the audit log.
Evidence-Grade Output and Audit Log
Given a processed image, When saved or uploaded, Then EXIF retains DateTimeOriginal, GPSLatitude/Longitude, and Orientation, and output resolution is ≥90% of original on both axes. Given processing completes, When metadata is written, Then an embedded XMP or sidecar JSON includes parcel polygon coordinates, applied mask regions, edge feather value, mask type/strength, processing mode (on-device/server), model/version IDs, and timestamps. Given integrity must be verifiable, When the image and manifest are stored, Then a SHA-256 hash of the final image and manifest ID are recorded in the audit log and associated to the post/bill in Duesly.
Manual Override & Review
"As a compliance officer, I want the ability to review and fine-tune the auto-crop and masking so that final evidence is accurate and defensible."
Description

Offer a post-capture review screen where users can adjust the crop boundary within allowed tolerances, add or refine masks, annotate with arrows or notes, and compare before/after. Include undo/redo, zoom, and snap-to-boundary controls. Track all edits with timestamps and user IDs, then save a locked evidence version for the compliance record. Provide admins with policy knobs to limit manual overrides or require secondary approval for overrides.

Acceptance Criteria
Adjust Crop Within Allowed Tolerance
Given a captured image with an auto-cropped parcel outline and admin setting ManualOverrideTolerance = T When the user drags any crop handle or edge Then the crop boundary cannot be moved beyond the parcel boundary by more than T in any direction And the crop cannot be reduced to exclude more than T inside the parcel And a visual constraint indicator appears when the limit is reached And the Save button remains disabled if the current crop violates tolerance And on release, the crop is persisted and the preview updates within 200 ms
Precision Controls: Snap, Nudge, Zoom & Pan
Given Snap-to-Boundary is toggled on and SnapThresholdPx = S When a crop edge is moved within S pixels of the parcel boundary Then it snaps to align with the boundary and displays a snap indicator Given the crop is selected When the user presses Arrow keys Then the crop nudges by 1 px (or 10 px with Shift) Given the review canvas When the user zooms via pinch or Ctrl/scroll Then zoom level ranges from 25% to 400%, centers on the cursor, and preserves clarity without jitter Given a zoomed-in view When the user pans via drag or space+drag Then panning is smooth at ≥30 FPS and constrained to image bounds
Privacy Masks Add & Refine
Given the Mask tool is active When the user adds a rectangle, circle, or polygon mask Then the mask cannot extend outside the current crop area, and vertices snap to the parcel boundary when within S pixels if Snap is on When editing a mask Then handles appear, geometry updates within 50 ms, and mask opacity (0–100%) and feather (0–20 px) are adjustable When attempting to place or extend a mask outside the parcel area Then the action is blocked and a message "Masks must remain within property boundary" is shown On export to evidence Then all masks are flattened into the image and cannot be toggled off in the locked version
Annotations (Arrows & Notes)
Given the Annotation tool is active When the user places an arrow or text note Then the item anchors to image coordinates and remains stable across zoom/pan And text notes allow up to 500 characters with sanitized input (no HTML) And styling options adhere to design tokens (size and color) When an annotation is moved, edited, or deleted Then the action is captured as a discrete history entry and reflected immediately in the preview On export to evidence Then annotations render on the image and are included in audit metadata with positions and a content hash
Undo/Redo and Edit History Logging
Given the user performs actions (crop adjust, mask edit, annotation change) When Undo (Ctrl/Cmd+Z) or Redo (Ctrl/Cmd+Y) is invoked Then the previous/next state is restored correctly, up to the last 50 actions After any new action following an Undo Then the Redo stack is cleared Each action is recorded with ISO 8601 timestamp, userId, actionType, and payload summary And the history is viewable in a review drawer On Save Evidence Then the edit history is sealed and immutable; further edits create a new version linked via parentVersionId
Before/After Compare View
Given a captured image and current edits When Compare is toggled on Then the UI shows Original vs Current in side-by-side or swipe modes as selected by the user And both panes maintain identical zoom level and synchronized panning When Compare is toggled off Then the UI returns to single-view state without altering edits And pixel scale fidelity between panes is identical with visual discrepancy ≤1 px
Locked Evidence & Admin Policy Controls
Given admin policies: ManualOverrideTolerance = T, RequireSecondaryApproval = A (true/false), DisableManualOverrides = D (true/false), ApprovalThreshold = P When D = true Then the review screen opens in view-only mode with manual controls disabled and a policy notice displayed When a user clicks Save Evidence and cumulative override magnitude ≤ P and A = false Then the system generates a locked evidence version with immutable ID, SHA-256 checksum, and embedded metadata (timestamp, parcelId, userId, versionId) saved to the compliance record When cumulative override magnitude > P or A = true Then the submission enters Pending Approval, editing is disabled for the submitter, and an approver can Approve/Reject with reason; the decision is logged with approver userId and timestamp Attempting to edit a locked evidence version is blocked; only "Create New Version" is allowed to start a new history chain
Evidence Metadata & Audit Trail
"As an HOA manager, I want a complete audit trail for each photo so that I can defend decisions in disputes or hearings."
Description

Attach verifiable metadata to each processed photo: parcel ID, GPS coordinates with accuracy radius, timestamp, device model/OS, capture method (camera vs. upload), processing version, and a cryptographic hash. Store the original securely with restricted access and strip EXIF from distributed copies. Surface key metadata in the compliance post within Duesly, support export for hearings, and log any subsequent redactions as a new version with diffs.

Acceptance Criteria
Camera Capture Metadata Completeness
Given a user captures a photo in-app for a known parcel And device GPS is available When the photo is auto-cropped and processing completes Then the stored record includes parcelId, gps.lat, gps.lng, gps.accuracyMeters, timestampUtc (ISO 8601), device.make, device.model, os.name, os.version, captureMethod="camera", processingVersion (semver), contentHash (SHA-256) And recomputing SHA-256 of the stored original matches contentHash And timestampUtc reflects server receipt time within ±5 seconds of server clock And parcelId matches the post’s parcel And processingVersion equals the currently deployed Parcel Guard processing version
Upload Metadata Handling and Defaults
Given a user uploads an image file to a compliance post for a known parcel When processing completes Then captureMethod="upload" And parcelId equals the post’s parcel And timestampUtc is the processing time in ISO 8601 UTC And processingVersion (semver) and contentHash (SHA-256 of the stored original) are recorded And gps fields are populated from file EXIF if present, else gps is null with accuracyMeters null And device fields are populated from EXIF if present, else device.* and os.* are null And recomputing SHA-256 of the stored original matches contentHash
Secure Original Storage and Access Control
Given a processed photo exists When stored Then the original image is stored in a restricted-access location not retrievable by general users or public links And only roles {Account Admin, Compliance Officer} can retrieve the original via an authenticated audit endpoint And all retrieval attempts (success or denied) are logged with actor, timestamp, and IP And the original file remains unmodified across versions; subsequent edits create new versions without overwriting the original
EXIF Stripped from Distributed Copies
Given a processed photo is displayed in the feed, downloaded from a post, or included in an export package When the image bytes are inspected Then no EXIF metadata blocks are present (e.g., no GPS*, Make, Model, or UserComment tags) And visual pixels are unchanged from the processed version And metadata shown to users is rendered from the app’s metadata store, not embedded in the image
Metadata Display in Compliance Post
Given a compliance post containing a processed photo is viewed by a board member When the post is rendered Then the UI displays: parcelId, capture timestamp (UTC), GPS coordinates and accuracy radius if available, captureMethod, processingVersion, and a shortened contentHash And if gps is null, the UI shows “GPS unavailable” And device details are viewable in a Details panel available to authorized roles
Hearing Export Package
Given a board member exports evidence for a post containing processed photos When export is requested Then a downloadable package is generated containing: a PDF summary, each processed image (with EXIF stripped), and a metadata.json with full metadata and version history including redaction diffs And the PDF summary includes parcelId, timestampUtc, captureMethod, GPS with accuracy if available, processingVersion, and contentHash for each photo And the export is available within 10 seconds for up to 20 photos And the hashes in metadata.json match recomputed SHA-256 of stored originals
Redaction Versioning and Diffs
Given a user applies a redaction (crop, blur, or mask) to an existing processed photo When the change is saved Then a new version is created with an incremented version number And the prior version remains immutable and retrievable by authorized roles And a diff record is stored describing the redaction type, coordinates/regions affected, and the actor and timestamp And the compliance post displays the latest version by default with a version history available to authorized roles
Admin Privacy & Compliance Settings
"As a board admin, I want configurable privacy and compliance settings so that Parcel Guard aligns with our community’s policies and local laws."
Description

Provide an admin panel to configure Parcel Guard policies per community: required GPS accuracy, geofence enforcement, masking categories, data retention durations, default consent prompts, and sharing permissions. Include preset profiles (strict, balanced, permissive) and in-product guidance explaining trade-offs. Enforce settings across mobile and web, integrate with role-based access control, and log policy changes with effective timestamps.

Acceptance Criteria
Per‑Community Policy Scope Isolation
Given Community A and Community B each have distinct Parcel Guard policies And an admin has permissions in both communities When the admin opens Admin Privacy & Compliance Settings for Community A Then the UI loads and persists only Community A’s policy values And switching to Community B displays only Community B’s policy values And a policy change saved in Community A does not alter Community B’s stored values And captures initiated by members of Community A enforce Community A’s policies, while Community B captures enforce Community B’s policies
Profile Presets With Guidance And Custom Overrides
Given the admin opens the Parcel Guard settings for a community When the admin selects the Strict preset profile Then all policy fields update to the Strict profile’s defined values And an inline guidance panel explains trade‑offs for Strict (privacy, compliance, usability) When the admin switches to Balanced or Permissive Then values update to that profile’s defined values and the corresponding guidance is shown When the admin edits any field after applying a preset Then the profile indicator changes to Custom and the edited values persist on save and reload
GPS Accuracy Requirement Enforcement
Given Required GPS Accuracy is set to X meters for the community And a mobile user is on the Parcel Guard capture screen When the device’s reported horizontal accuracy is greater than X meters Then the capture action is disabled and an accuracy warning is displayed When the device’s reported horizontal accuracy is less than or equal to X meters Then the capture action becomes enabled And the captured photo metadata stores the measured accuracy value and the policy version applied
Parcel Geofence Enforcement
Given Geofence enforcement is enabled for the community And a geofence tolerance T meters is configured When a user attempts to capture outside the parcel boundary by more than T meters Then the capture is blocked and a message explains it must be taken within the property boundary When a user attempts to capture within the boundary or within T meters of the boundary Then the capture is allowed And capture metadata records inside/outside status and the policy version applied
Masking Categories Enforcement
Given masking categories Faces, License Plates, and Neighboring Parcels are enabled in settings When a new photo is uploaded or captured Then the system applies masking for all enabled categories before storage/display And if a category is disabled in settings, masking for that category is not applied on subsequent captures And the media detail view indicates which masking categories were applied And unmasked originals are not accessible to roles without explicit permission
Data Retention With Effective Policy Versioning
Given Data Retention is set to N days for the community And a policy change log records effective timestamps for each version When a photo reaches N full days since capture under the policy version effective at capture time Then the photo and associated derivatives are permanently deleted And an audit entry records the deletion event with item ID, timestamp, and policy version used When the retention value is changed to M days with a new effective timestamp Then items captured before the change use the prior version’s N days, and items captured after use M days
RBAC And Sharing Permissions Enforcement Across Clients
Given only users with ManagePrivacySettings permission can edit Parcel Guard policies When a user without this permission opens the settings Then fields are read‑only or hidden and save actions are disabled When Sharing Permissions is set to Board Only Then only users in authorized roles can view captured photos in both web and mobile clients When Sharing Permissions is set to Owner + Board Then the lot/unit owner and authorized roles can view, and other users are denied with an access error And all access denials and policy edits are recorded in the audit log with user, timestamp, and scope
Offline Capture & Deferred Sync
"As a field inspector, I want to capture and process photos offline so that I can work reliably in areas with poor reception."
Description

Enable photo capture and boundary guidance with limited or no connectivity by pre-caching parcel boundaries and lightweight map tiles. Queue processing and uploads for background sync with conflict resolution and user-visible status. Clearly indicate stale boundaries and prompt for re-validation upon reconnection. Ensure power-efficient retries and respect metered network settings.

Acceptance Criteria
Pre-cache Parcel Data for Offline Use
Given I have network connectivity and am viewing Property A in Parcel Guard When I select 'Make available offline' Then the app displays the estimated download size and required storage before starting And the app downloads parcel boundary geometry, guidance assets, and minimum map tiles (zoom levels 16-18) covering the property plus a 50 m buffer And the assets are accessible with the device in Airplane Mode And an 'Offline' badge appears for Property A within 2 seconds of successful cache And if storage is insufficient, the action is aborted with a clear error and no partial cache is used
Offline Capture with Boundary Overlay
Given my device has no data connectivity And Property A has been made available offline When I open the camera via Parcel Guard for Property A Then the parcel boundary overlay renders within 1 second of camera open And on-screen framing guidance is shown and updates from device sensors without network And photos can be captured and are saved to the local queue with timestamp, GPS fix (or last known location), and boundary version ID And if GPS is unavailable, the app warns 'Location unavailable; using last known location' and allows capture with this metadata
Deferred Sync Queue and User-Visible Status
Given I have one or more queued captures When network connectivity becomes available Then background sync begins within 10 seconds subject to user network preferences And each item transitions through visible states: Queued -> Syncing -> Succeeded or Failed And a Sync Center displays counts and per-item progress, error messages, and retry ETA And if on a metered connection and 'Wi-Fi only' is enabled, uploads are deferred until Wi-Fi unless the user overrides for this session
Conflict Resolution on Boundary Updates
Given queued captures reference boundary version V1 And the server has boundary version V2 for Property A When sync starts Then the user is prompted to re-validate alignment before submission And if the V1->V2 geospatial delta is <= 2 meters everywhere, the system auto-associates photos to V2 and logs the auto-resolution And if any delta exceeds 2 meters, the item is marked 'Needs review' and does not auto-submit And all decisions are recorded in an immutable audit log with timestamp and actor
Stale Boundary Indication and Re-Validation Prompt
Given my local cache for Property A is older than 7 days or a newer boundary exists on the server When I view Property A while offline Then a 'Stale boundary' indicator is displayed with the last-updated date And capture is allowed but the queued item is labeled 'Stale boundary' And upon reconnection, the app prompts to refresh boundaries and to re-validate any queued items before upload
Power-Efficient Retries and Background Behavior
Given an upload fails due to a transient network error or timeout When the system schedules retries Then retries use exponential backoff starting at 30 seconds and capping at 15 minutes, with jitter And no more than 4 background wake-ups per hour are used for sync when the app is backgrounded And when OS Low Power Mode is enabled, background sync pauses with a visible 'Paused due to Low Power Mode' status and resumes automatically when disabled or when the app is foregrounded And retries respect OS background execution limits without unexpectedly foregrounding the app

Proof Stamp

Applies a tamper‑evident seal that links each redacted image to its original, mask coordinates, and timestamp. Exports include a verification hash and activity log so you can prove what was hidden (and only that), speeding disputes and satisfying auditors.

Requirements

Tamper-Evident Seal Generation
"As a compliance officer, I want a tamper-evident seal created at save time so that I can prove the file’s integrity and authorship during reviews and disputes."
Description

Automatically generate a cryptographic seal whenever a redacted image is saved, binding the redacted asset to its original file hash, redaction mask coordinates, editor identity, and timestamp. Use canonicalized JSON manifests hashed with SHA-256 and signed with Ed25519 (or equivalent) to provide non-repudiation. Persist the manifest as an immutable artifact linked to the post in Duesly’s feed and expose the seal ID and verification hash for downstream use and auditing.

Acceptance Criteria
Seal Generated on Redacted Image Save
Given a user saves a redacted image on a post When the save operation completes successfully Then a cryptographic seal is generated automatically and associated to the redacted asset and its post And the manifest binds the redacted asset to: originalFileHash (SHA-256 hex of original bytes), redactionMasks, editorUserId, and timestamp And each redaction mask is recorded with an unambiguous coordinate schema: top-left origin, integer pixel units relative to the original image, includes imageWidth and imageHeight; rectangles use [x,y,width,height] And timestamp is ISO 8601 UTC (e.g., 2025-08-16T12:34:56Z) And the verificationHash equals the SHA-256 digest of the canonicalized manifest
Deterministic Manifest Canonicalization
Given two JSON manifests with identical semantic data but different key orders and whitespace When they are canonicalized and hashed with SHA-256 Then the canonical byte representations are byte-for-byte identical And the verificationHash values are identical 64-character lowercase hex strings And changing any single field value produces a different verificationHash
Ed25519 Signature and Non-repudiation
Given a generated manifest digest (SHA-256 of canonicalized JSON) When the system signs the digest using Ed25519 with the platform's private key Then the manifest includes fields: signature (base64), keyId, and algorithm = "Ed25519" And signature verification with the published public key succeeds And any alteration to the manifest or signature causes verification to fail
Immutable Persistence and Feed Linkage
Given a generated seal and its manifest When the manifest is persisted Then it is stored immutably and addressed by its verificationHash And attempts to modify or delete the stored manifest via public interfaces are rejected and logged And the related Duesly post stores the sealId and a pointer to the stored manifest And retrieving by sealId returns the exact originally stored manifest bytes and metadata
Seal ID and Verification Hash Exposure (UI and API)
Given a post with a redacted image and generated seal When viewing the post in the feed UI Then the UI displays sealId and verificationHash as copyable values And when fetching the post via API Then the response includes fields sealId and verificationHash matching the stored manifest And verificationHash is a 64-character lowercase hex string
Versioning on Subsequent Edits
Given a redacted image with an existing seal When the image is edited again and saved Then a new manifest and seal are generated with a new sealId and verificationHash And previous manifests and seals remain unchanged and retrievable And the post history lists each seal with its timestamp and editorUserId
Transactional Failure Handling
Given a failure during any step of manifest generation, hashing, signing, or persistence When saving a redacted image Then the save is aborted and no post update is published And the user sees a clear error indicating which step failed And no partial or orphaned seals/manifests are exposed And the error is logged with a correlation ID for auditing
Redaction Metadata Capture
"As a board member, I want exact mask details recorded so that I can demonstrate to auditors what was hidden and nothing more."
Description

Capture and store precise redaction metadata including mask coordinates, shapes, pages, and mask types (e.g., text, faces, numbers) without exposing original content in the manifest beyond a hashed reference. Ensure the manifest proves exactly what areas were concealed and only those areas, enabling auditors to validate scope without access to the unredacted file. Associate all metadata with UTC timestamps, user IDs, and post IDs for traceability.

Acceptance Criteria
Capture Mask Geometry and Types per Page
Given a user applies multiple redactions of varied shapes across one or more pages When the redacted export is generated Then the manifest records for each mask: mask_id (UUID), page_index (0-based), shape_type (rectangle|polygon|ellipse|freehand), coordinates (pixel integers at export resolution), and mask_type (text|face|number|signature|other) And the coordinates, when re-rendered from the manifest, reproduce the mask coverage within ±1 pixel of the export And masks are grouped by page with masks ordered by creation time
Manifest Excludes Original Content Beyond Hashed Reference
Given a manifest is generated for a redacted export Then the manifest includes only cryptographic hashes for source and export (original_file_sha256, redacted_file_sha256) And the manifest contains no raw bytes, thumbnails, or OCR text derived from the unredacted source And byte-for-byte comparison confirms that no contiguous 32-byte segment from the source file appears in the manifest And removing the hashes leaves no data that enables reconstruction of any hidden content
Scope Validation Without Unredacted Access
Given the manifest and the associated redacted export When a verifier rasterizes all mask polygons at the export’s pixel dimensions Then 100% of pixels exhibiting the redaction fill signature lie inside the rasterized mask regions And 0 pixels outside the rasterized mask regions exhibit the redaction fill signature And any edge variance does not exceed a 1-pixel anti-aliasing tolerance
Traceability: UTC Timestamps, User IDs, and Post IDs
Given any mask is created, edited, or removed When the manifest is saved Then the manifest records created_at_utc and updated_at_utc in ISO 8601 Z, the acting user_id, and the post_id And timestamps are in UTC with a 'Z' suffix and are non-decreasing within a post’s history And user_id and post_id conform to UUID format and resolve to existing records
Redaction Activity Log with Before/After Deltas
Given a sequence of redaction actions in a session When the manifest is exported Then an activity_log array lists each action (add|move|resize|retype|delete) with mask_id, actor user_id, occurred_at_utc, and before/after geometry and mask_type as applicable And the activity_log_sha256 (hash over a canonicalized log) is included in the manifest And recomputing the hash from the exported log exactly matches activity_log_sha256
Tamper-Evident Verification Hash Linking Masks and Files
Given the manifest, the redacted export, and original_file_sha256 When a verifier recomputes the verification hash over the canonical manifest fields (file hashes, mask set, timestamps, ids, and activity_log hash) Then the computed verification hash matches the embedded verification hash And any change to mask coordinates, types, page indices, ids, or log entries causes verification to fail
Proof Export Bundle
"As a property manager, I want to export a complete proof package so that third parties can verify authenticity without needing access to Duesly."
Description

Provide a one-click export that packages the redacted file, the signed manifest (seal), a human-readable summary, a machine-readable JSON manifest, the verification hash, and the redaction activity log into a self-contained bundle (ZIP/PDF). Include a QR code and shareable link to an in-app verification page. Ensure exports omit PII except where hashed, and include checksum files for the bundle and each artifact to support offline verification.

Acceptance Criteria
Complete Artifact Set in Export Bundle
Given a redacted image exists with a Proof Stamp and the user has export permission When the user clicks “Export Proof Bundle” Then the system generates a single self-contained bundle in the selected format (ZIP or PDF) And the bundle includes all of the following artifacts: the redacted file, a signed manifest (seal), a human‑readable summary, a machine‑readable JSON manifest, a verification hash, and a redaction activity log And the bundle also includes checksum files for the bundle itself and for each included artifact And no additional, undeclared artifacts are present
Valid Signature and Verification Hash
Given an exported bundle has been generated When the signed manifest (seal) is verified using Duesly’s public key Then the signature validation succeeds And the manifest JSON referenced by the seal matches byte-for-byte the manifest included in the bundle And the verification hash file value matches the corresponding value recorded in the manifest And modifying any included artifact causes either signature or checksum validation to fail
QR Code and Shareable Link Open Verification Page
Given the exported bundle contains a summary with a QR code and a shareable link When a user scans the QR code or opens the shareable link Then the in‑app verification page loads successfully without requiring authentication And it displays the export identifier, timestamp, verification hash, and a summary of redactions And it references the same manifest hash as included in the bundle
PII Omission and Hashing Compliance
Given the exported bundle is inspected (including file contents, filenames, metadata, and embedded objects) When checked against Duesly’s PII policy (e.g., names, emails, phone numbers, unit/address, banking details) Then no raw PII is present anywhere in the bundle And any required identifiers appear only as salted hash values as defined in the manifest And document metadata (e.g., EXIF/PDF properties) contains no PII And the QR code and shareable link embed only non‑PII identifiers (e.g., export ID, hash)
Per‑Artifact and Bundle Checksums Support Offline Verification
Given the exported bundle and its checksum files When SHA‑256 (or the specified algorithm) is computed offline for the bundle and each artifact Then each computed value matches the corresponding checksum entry included in the bundle And offline verification can be completed without any network access And any single‑byte change to any artifact produces a checksum mismatch for that artifact and, if applicable, the bundle
ZIP and PDF Packaging Behavior
Given the user selects a packaging format prior to export When ZIP is selected Then the download is a .zip archive that contains each artifact as a separate file along with checksum files And the archive can be opened by standard ZIP tools without errors When PDF is selected Then the download is a single PDF where the human‑readable summary is the main document and other artifacts are included as attachments or appendices And the checksums are included in the PDF (as an appendix and/or attached checksum files)
Redaction Activity Log Completeness and Alignment
Given the redaction session history for the exported image When the bundle is generated Then the redaction activity log lists every redaction action in chronological order with: ISO‑8601 UTC timestamp, action type, mask coordinates, and user identifier in hashed form And the number of entries equals the number of redaction actions performed for that export And the activity log values (e.g., mask coordinates and timestamps) align with the values referenced by the signed manifest (seal) and manifest JSON
In-App Verification Viewer
"As a disputes specialist, I want a one-click verification view so that I can quickly confirm authenticity and scope during a challenge."
Description

Implement an in-app verifier that accepts a proof bundle or a redacted file plus manifest and performs signature validation, hash comparison, and mask coverage checks. Display pass/fail results, seal details, and a visual overlay showing masked regions versus the redacted file. Support optional verification against the original file when the user has permission, and never leak unredacted content to unauthorized users.

Acceptance Criteria
Proof bundle validation and seal details display
Given a valid proof bundle containing a redacted file, manifest, signature, and activity log When the user opens it in the verifier Then the verifier validates the digital signature with the configured public key and confirms the manifest hash matches the redacted file bit-for-bit And Then it verifies mask coverage: each redacted pixel is fully encompassed by at least one mask region and no mask region lies outside image bounds And Then it displays Verification Passed with seal details: signer ID, timestamp (UTC, ISO-8601), signing algorithm, verification hash (e.g., SHA-256), redacted file fingerprint, manifest version And Then it shows mask-region count and enables export of the verification hash and activity log (JSON) without accessing any unredacted content
Redacted file plus manifest verification input pair
Given a redacted file and a separate manifest uploaded together When required fields (original file hash, mask coordinates, signature, algorithm identifiers, manifest version) are present and well-formed Then the verifier performs the same validations as for a proof bundle and shows a pass/fail result with seal details Given required fields are missing or malformed When the user submits Then the verifier blocks verification and lists the specific missing/invalid fields with precise paths/line numbers
Visual overlay of masked regions
Given a successfully verified file When the user toggles Show masks Then the viewer renders overlays that align with the redacted regions within ±1px or ±0.2% of image dimensions (whichever is greater) across 10%–800% zoom and during pan And When the user hovers or keyboard-focuses a region Then that region highlights using a colorblind-safe palette and displays its coordinates and area; keyboard navigation can move to next/previous region And Then toggling the overlay on/off never alters the underlying file
Original-file cross-check for authorized users without content exposure
Given the user has View Original for Verification permission and the system can securely access the original file When the user initiates Verify against original Then the verifier compares the original file hash to the manifest’s original hash and performs a pixel-level mask coverage check And Then only computed results (pass/fail and aggregate statistics) are shown; no unredacted pixels or thumbnails of the original are rendered client-side or stored And Then network traffic confirms no original bytes are transmitted to clients lacking the permission
Prevent unauthorized original verification and audit the attempt
Given the user lacks permission or the original file is unavailable When the user attempts Verify against original Then the verifier denies the action with a clear message and returns HTTP 403 or 404 as appropriate, leaving bundle-level verification available And Then the attempt is appended to the activity log with user ID, timestamp, and reason, without exposing unredacted content or disallowed metadata
Tamper detection and error handling
Given a tampered or invalid input (e.g., signature invalid, hash mismatch, out-of-bounds mask coordinates, unsupported manifest/file version, or corrupted archive) When processed by the verifier Then the verifier displays Verification Failed with a specific failure code and human-readable cause and disables the overlay And Then no partial results are labeled as pass and the failure is recorded in the activity log
Performance and resource usage thresholds
Given an image up to 25 MB with up to 10,000 mask regions When verification runs on a typical device (≥2-core CPU, 8 GB RAM) or the standard server path Then initial verification completes within 3 seconds at the 95th percentile and within 6 seconds worst-case, overlay interactions maintain ≥45 FPS at 4K resolution, and operations exceeding 500 ms show a progress indicator And Then peak memory usage for the verifier stays under 500 MB during verification and overlay rendering
Signing Key Management and Rotation
"As a security administrator, I want managed signing keys with rotation so that proof seals remain trustworthy and verifiable long-term."
Description

Manage signing keys used for sealing via a secure KMS/HSM with periodic rotation, key versioning, and revocation. Embed key IDs in manifests and publish public keys via a JWKS endpoint for external verification. Provide admin controls and audit trails for key lifecycle events to keep seals verifiable over time without breaking existing proofs.

Acceptance Criteria
Generate Non-Exportable Signing Key in KMS
Given an authorized service requests key creation in KMS/HSM, When the request is executed, Then a new asymmetric non-exportable key is created with usage=sign/verify and the configured algorithm (RSA-2048 or ECDSA P-256). And Then the key is labeled with environment, service, unique kid, and created_at. And Then KMS policy restricts Sign to the sealing service role and denies Export/GetPrivateKey (403) with audit log entries on attempts. And Then an audit event records actor, action=create, kid, alg, timestamp, and reason. And Then key health status is available to the service within 30 seconds of creation.
Automatic Time-Based Key Rotation Without Breaking Verification
Given rotationPolicy.days=90 and a key age >= 90 days, When the rotation job executes, Then a new key version is created and set active for signing within 5 minutes. And Then the previous key version remains enabled for verification and its public key is retained in JWKS for >= 365 days. And Then all new seals after cutover include the new kid; prior seals continue to verify. And If rotation fails, Then on-call is alerted within 2 minutes and retries occur with exponential backoff; no seals are signed with an expired key. And When a user triggers "Rotate Now", Then the same steps occur with audit logging.
Manifest and Seal Embed Key ID and Version
Given a redacted image is sealed, When generating the manifest, Then fields kid, alg, and key_version match the signing key used. And Then the manifest timestamp is RFC3339 UTC and within ±2 minutes of system time. And Then verification using the JWKS key with matching kid succeeds; verification fails if kid is missing or mismatched.
Public Keys Published via JWKS Endpoint
Given a client requests GET /.well-known/jwks.json over HTTPS, When successful, Then response is 200 with a valid JWKS (RFC 7517) containing all active and retired (non-revoked) public keys with kty, use=sig, alg, kid, and appropriate parameters (n,e or crv,x,y). And Then response includes Cache-Control: public, max-age=300 and an ETag, and reflects key changes within 2 minutes. And Then the endpoint maintains >=99.9% monthly availability, supports >=100 RPS, and returns 429 with Retry-After on limits. And Then no private key material is exposed; security headers are present (HSTS, no wildcard CORS).
Key Revocation and Continued Verifiability
Given an admin revokes a key version with justification, When confirmed, Then signing with that key is disabled within 1 minute and subsequent seals use the latest active key. And Then JWKS continues publishing the revoked key's public parameters for >=365 days with "revoked": true, enabling verification of historical seals. And Then any attempt to sign using a revoked kid returns 403 and is audit logged. And Then the revocation event captures who, when, why, kid, and is tamper-evident in the audit trail.
Admin Controls with RBAC and Dual Approval
Given the Admin Console or API, When a user with role=SecurityAdmin initiates create/rotate/revoke, Then rotate and revoke require dual approval from a second authorized user before execution. And Then endpoints are protected by scoped OAuth2 tokens enforcing least privilege. And Then the UI/API expose current keys with status (active, retired, revoked), age, last_used_at, and next_rotation_at. And Then unauthorized users receive 403 and events are audit logged.
Immutable Audit Trail for Key Lifecycle
Given any key lifecycle event (create, rotate, retire, revoke, policy change), When it occurs, Then an append-only audit record is written within 5 seconds containing event_type, kid, actor_id, actor_ip, timestamp (RFC3339 UTC), outcome, and hash chaining to the previous record. And Then audit logs are retained for 7 years, exportable as CSV and JSONL, and verifiable via a published root hash. And Then audit search supports filters by date, action, actor, kid and returns 10k-record queries in <=5 seconds. And Then system clocks are NTP-synchronized with drift <100ms across services.
Role-Based Proof Access Controls
"As an auditor, I want access to proof artifacts without exposure to originals so that privacy is preserved while enabling verification."
Description

Enforce granular permissions determining who can create seals, view manifests, export proof bundles, and access originals. Allow auditors to view proof artifacts and verification results without exposure to unredacted content, while restricting original-file access to authorized roles. Log all proof-related access for compliance.

Acceptance Criteria
Create Seal Permission Enforcement
Given a user without permission proof.create_seal on record R When they POST /proofs/seal Then the response is 403 Forbidden, no sealId is created, and no changes are persisted to record R. Given a user with permission proof.create_seal on record R When they POST /proofs/seal with valid payload Then the response is 201 Created with sealId, and the seal is associated to record R.
Auditor Access to Proof Artifacts Without Originals
Given a user with role Auditor having permissions proof.view_manifest and proof.view_verification_result but not proof.view_original When they open the proof details for record R Then they can view manifest fields, verification results, redacted previews, and activity log; no original file bytes are transmitted; any attempt (UI or API) to fetch originals returns 403 and is logged. Given an Auditor viewing any preview When a thumbnail or preview is rendered Then it is derived only from the redacted asset and contains zero unredacted pixels.
Export Proof Bundle Permission Gating
Given a user without permission proof.export_bundle When they request an export for record R Then the response is 403 Forbidden and no archive is generated. Given a user with permission proof.export_bundle but without proof.view_original When they export a proof bundle Then the archive contains only redacted assets, masks, manifest, verification hash(es), and activity log, excludes any original file bytes or URLs, and the archive integrity matches the manifest checksums. Given a user with permissions proof.export_bundle and proof.view_original When they export a proof bundle Then inclusion of originals is controlled by an explicit includeOriginals flag; if false, originals are excluded; if true, originals are included and access is logged as success.
Original File Access Restriction
Given a user without permission proof.view_original When they request GET /proofs/{id}/original Then the response is 403 Forbidden, Content-Length is 0, and the attempt is logged with result=denied and reasonCode=missing_permission. Given a user with permission proof.view_original When they request GET /proofs/{id}/original Then the response is 200 OK with the original file whose SHA-256 equals stored original_hash and the access is logged with result=success.
Comprehensive Proof Access Logging
Rule: Every proof-related action (seal creation, manifest view, verification run, export request, original-file access, and any denied attempt) writes an append-only log entry with fields: actorId, role, action, resourceId, timestamp (UTC ISO8601), clientIp, userAgent, result (success|denied|error), reasonCode, and correlationId. Rule: Log entries are immutable to all roles and are retrievable by users with permission proof.view_audit_log via API/UI within 5 seconds of the action. Rule: Attempts to delete or edit log entries are blocked for all roles; such attempts are themselves logged as denied.
Immediate Permission Change Enforcement
Given a user currently holds proof.view_original and an admin revokes that permission When the user attempts to access any original within 60 seconds of the change Then access is denied with 403 and the attempt is logged with reasonCode=permission_revoked. Given a user is granted proof.export_bundle During an active session When they retry the export request Then it succeeds without requiring a new login and the success is logged. Rule: Permission cache TTL is ≤60 seconds for both UI and API requests.
Auditor Verification Links Without Original Exposure
Given a user with permission proof.share_auditor_link but without proof.view_original When they generate an auditor verification link for record R Then the link grants access only to manifests, verification results, and redacted previews, never to original content; the link expires within 7 days, is revocable, and all accesses are logged. Given any recipient using an auditor verification link When they attempt to access an original file Then the response is 403 Forbidden and the attempt is logged with result=denied. Given a recipient using an auditor verification link When they run verification Then the verification succeeds using only redacted artifacts and published hashes, without requiring originals.
Proof APIs and Webhooks
"As an integrator, I want APIs and webhooks for proof artifacts so that my records system can automatically ingest and track verification data."
Description

Expose REST endpoints to retrieve manifests, list proof stamps by post, and download export bundles, with pagination, rate limiting, and OAuth-based auth. Provide webhooks that fire on seal creation, export generation, and verification events so external systems can ingest or mirror artifacts. Include versioned schemas and example payloads for integrators.

Acceptance Criteria
OAuth2 Authentication and Scopes for Proof APIs
Given a registered integration with client credentials and allowed scopes When it requests an access token using OAuth 2.0 client credentials for scopes proof.read and/or proof.write over TLS 1.2+ Then an access token is issued with token_type "Bearer", expires_in, and the approved scopes And requests to any Proof API without a valid bearer token return 401 with a WWW-Authenticate error And requests made with a valid token lacking the required scope return 403 And tokens expired or revoked result in 401 invalid_token
Retrieve Proof Manifest by ID
Given a tenant-owned proof manifest exists When GET /proof/manifests/{manifest_id} is called with a valid proof.read token Then the API returns 200 application/json with a body conforming to the published Manifest schema (current major version) And the payload includes fields id, post_id, schema_version, created_at, stamp_count, verification_hash, and links And ETag and Last-Modified headers are present And requesting a non-existent or unauthorized manifest returns 404
List Proof Stamps by Post with Pagination
Given a post with N associated proof stamps When GET /posts/{post_id}/proof-stamps?page[size]=S&page[after]=C is called with a valid proof.read token Then the API returns 200 with at most S items sorted by created_at descending And the response includes pagination cursors (next/prev) per the documented schema And page[size] is clamped to the documented maximum when exceeded And an invalid or expired cursor returns 400 And a non-existent or unauthorized post_id returns 404
Download Export Bundle with Verification Hash
Given an export bundle exists and is authorized for the caller When GET /proof/exports/{export_id}/download is called with a valid proof.read token Then the API returns 200 application/zip with Content-Disposition attachment And the archive contains manifest.json, activity_log.json, redacted assets, and a verification_hash file matching the manifest hash And a Digest or checksum header is provided for the archive (e.g., sha-256) And requesting a non-existent or unauthorized export_id returns 404
API Rate Limiting and 429 Behavior
Given repeated requests exceeding the documented per-client limit within the window When additional requests are made to any Proof API endpoint Then the API responds 429 Too Many Requests with Retry-After and RateLimit-Reset headers And non-exceeding responses include RateLimit-Limit and RateLimit-Remaining headers And write endpoints are not partially applied when a 429 is returned
Webhooks for Seal, Export, and Verification Events
Given a tenant has configured a webhook endpoint and shared secret When a proof seal is created, an export is generated, or a verification completes Then the system enqueues and delivers an event with type one of proof.seal.created, proof.export.generated, proof.verification.completed within 60 seconds And the POST body conforms to the published event schema and includes event_id, event_type, schema_version, resource_id, occurred_at, and resource_url And an HMAC-SHA256 signature header over the raw body (using the shared secret) is included And a 2xx response from the receiver is treated as success; non-2xx triggers exponential backoff retries up to the documented limit And duplicate deliveries include the same event_id and Idempotency-Key header
Versioned Schemas and Example Payloads Available to Integrators
Given an integrator requests schemas and examples for Proof APIs and webhooks When fetching the schema index and specific versions Then JSON Schemas are published per resource and event with semantic versioning (e.g., v1.x), and are retrievable via documented URLs And all API responses and webhook payloads include a schema_version field matching a published schema And example request/response bodies for each endpoint and event are available and validate against their schemas And requesting an unsupported major version results in a 406 Not Acceptable (or documented equivalent)

Brush Assist

Fast manual tools that snap masks to edges as you swipe—perfect for tricky reflections or signage. Undo/redo, compare‑view, and per‑mask notes keep edits precise and documented without needing pro photo software.

Requirements

Smart Edge‑Snap Brush
"As a community manager, I want the brush to snap to object edges while I paint so that I can highlight issues accurately without needing pro photo software."
Description

Implement a brush tool that magnetically snaps mask boundaries to detected edges (e.g., signage, windows, fences) as the user swipes, enabling fast, precise selections on photos used in compliance and announcements. Provide adjustable snap strength, brush size, hardness, feathering, and an eraser mode. Support mouse, touch, and stylus input with low-latency rendering and on-canvas visual feedback at interactive frame rates. Run edge detection client-side to keep images private and responsive, with graceful fallback to freehand when edges are ambiguous. Store masks as resolution-independent paths for non-destructive editing and reuse across exports. Integrate the tool into Duesly’s media editor for posts and compliance cases.

Acceptance Criteria
Edge Snap Accuracy on High-Contrast Boundaries
Given a photo with clear, high-contrast edges (e.g., signage, window frames) And snap strength set to 50% When the user strokes within 8 px of an edge for at least 100 px of path length Then the resulting mask path deviates ≤ 2 px RMS from the detected edge over the stroked segment And corners ≥ 70° are preserved with angle error ≤ 5° And the path is continuous with no gaps > 1 px
Graceful Fallback to Freehand on Ambiguous Edges
Given a region where edge confidence drops below 0.5 for ≥ 16 px When the user continues the same stroke Then the tool draws freehand for that segment with no mode switch popup and ≤ 50 ms latency increase And on-canvas feedback indicates freehand segment distinctly from snapped segments And when confidence rises to ≥ 0.5, snapping resumes within 30 ms And setting snap strength to 0% yields pure freehand for the entire stroke
Adjustable Brush Parameters
Given the brush settings panel When brush size is adjusted between 1–300 px Then the cursor ring and stroke diameter reflect the value immediately (<30 ms) and the applied mask matches ±1 px When hardness is adjusted 0–100% Then the edge falloff follows a corresponding hardness curve visible in the preview ring When feathering is set 0–50 px Then the mask feather radius equals the value ±1 px on inspection When snap strength is adjusted 0–100% Then 0% produces no snapping and 100% maximizes snapping, with intermediate values interpolated linearly
Eraser Mode Removes Mask Precisely
Given an existing mask When Eraser mode is enabled Then strokes subtract from the mask using current size, hardness, and feather settings And Undo immediately restores the last erased segment And with snap strength > 0%, eraser follows edge snapping with the same accuracy metrics as the brush
Multi-Input Support Across Devices
Given devices providing mouse, touch, and stylus input When drawing a continuous 5-second stroke with each input Then the stroke is tracked without discontinuities and input events are processed at ≥ 60 Hz And palm rejection prevents stray touches during stylus use with ≤ 1 false stroke per 10 minutes of testing And two-finger pan/zoom works before and after strokes and does not occur during a single-finger/stylus stroke
Low-Latency Rendering, Client-Side Processing, and Visual Feedback
Given a 12 MP JPEG in the editor When the user draws continuously for 5 seconds Then average render frame rate is ≥ 45 FPS and no frame exceeds 100 ms And input-to-pixel latency is ≤ 50 ms median and ≤ 80 ms 95th percentile And snap preview outlines update within 30 ms of pointer movement And all edge detection and masking computations execute client-side, verified by editing functioning offline and no network requests containing image pixel data during editing
Non-Destructive Vector Masks and Workflow Integration
Given a completed mask When saving, closing, and reopening the editor Then the mask persists as resolution-independent vector paths with editable control points And exporting at 1x, 2x, and 4x scales keeps the mask alignment within 1 px at each scale And the same mask can be attached to a Post and to a Compliance Case without re-creation And disabling or deleting the mask restores the original image with no irreversible changes
Multi‑Mask Layer Management
"As a compliance officer, I want to manage multiple masks on a photo so that I can document several violations in a single image efficiently."
Description

Enable creation and management of multiple masks per image with layer-like controls: name, color-code, reorder, show/hide, lock/unlock, and set per-mask opacity and feather. Ensure non-destructive edits and auto-save of mask state with the associated image asset. Limit maximum masks per image to maintain performance while accommodating typical use (e.g., up to 20). Seamlessly integrate with per-mask notes and export so each mask remains identifiable in cases and announcements.

Acceptance Criteria
Create, name, and color-code masks
Given an image is open in Brush Assist When the user creates a new mask Then the mask is added to the mask list as the active selection with a default name "Mask <n>" and a distinct color from the palette And the total mask count increases by 1 Given a mask is selected When the user renames the mask to a 1–50 character value Then the new name displays in the list and persists after page reload And leading and trailing whitespace is trimmed Given a mask is selected When the user changes its color using the color palette Then the swatch updates immediately and persists after page reload And the palette provides at least 10 distinct options
Reorder mask layers
Given at least 3 masks exist When the user drags a mask in the list to a new position Then the list order updates and the compositing order reflects the new list order And the new order persists after page reload Given a mask is selected When the user reorders masks repeatedly (5 or more times) Then each action is recorded in the undo stack and can be undone and redone step-by-step without data loss
Show/Hide and Lock/Unlock per mask
Given a mask exists When the user toggles its visibility off Then the mask's effect is removed from the preview without deleting its data And its visibility state persists after page reload Given a mask exists When the user locks the mask Then brush and edit actions cannot modify that mask And the UI indicates the locked state And the lock state persists after page reload
Per-mask opacity and feather controls
Given a mask is selected When the user sets opacity to any value between 0% and 100% inclusive Then the preview updates within 150 ms to reflect the change And the value is stored with 1% precision and persists after page reload Given a mask is selected When the user sets feather radius to a value between 0 and 100 px inclusive Then the preview updates within 150 ms And the value is stored with 1 px precision and persists after page reload Given opacity or feather values are changed When the project is exported Then the exported metadata includes the mask's opacity and feather values
Non-destructive editing and undo/redo
Given an image has one or more masks When the user performs a reset of all masks Then the original image pixels remain unchanged and the canvas returns to the unmasked view Given the user makes a series of mask edits (create, delete, reorder, rename, opacity, feather) When they press undo and then redo Then each step reverses and then reapplies the change in correct sequence without data loss
Auto-save mask state with image asset
Given a user makes any change to mask state When 2 seconds pass without further edits or the user navigates away Then the system auto-saves the current mask state with the image asset And a Saved indicator appears within 500 ms of save completion Given the browser tab is closed or the app unexpectedly reloads When the image is reopened Then the last auto-saved mask state is restored, including order, visibility, lock, opacity, feather, names, colors, and notes Given a network interruption occurs during auto-save When connectivity resumes Then pending changes are retried automatically And if save fails after 3 retries, the user is notified without losing local changes
Mask limit enforcement and exportable identification with notes
Given an image has 20 masks (the maximum) When the user attempts to add another mask Then the action is prevented, an inline message "Maximum 20 masks per image" is shown, and the add control is disabled until a mask is deleted Given a mask is selected When the user adds or edits a note up to 500 characters Then the note is saved with that mask, persists after page reload, and is deleted if the mask is deleted Given masks have names, colors, order, visibility, lock, opacity, feather, and notes When the project is exported and referenced in cases and announcements Then each mask is included in the export metadata with a unique ID and its attributes, and consuming views display the mask name to keep it identifiable
Undo/Redo History Stack
"As an editor, I want to undo and redo my mask changes so that I can refine selections without starting over."
Description

Provide robust undo/redo for all mask operations (paint, erase, move, reorder, opacity changes) using a command history with at least 50 steps per image. Include keyboard shortcuts and toolbar controls, with atomic operations to prevent partial state. Persist history during the editing session and protect against data loss with periodic snapshots. Ensure consistency across multiple masks and prevent cross-image contamination of history.

Acceptance Criteria
Undo/Redo Painting and Erasing Up To 50 Steps
Given an open image with Brush Assist and an active mask And 55 separate paint/erase strokes have been completed (each from mousedown to mouseup) When the user presses Undo 50 times via Ctrl/Cmd+Z and the toolbar button Then the most recent 50 stroke operations are undone in reverse order And the 5 earliest operations remain applied And the Undo control is disabled on the 50th undo When the user presses Redo 50 times via Ctrl/Cmd+Shift+Z and the toolbar button Then the 50 undone operations are reapplied in original order And the Redo control is disabled when no redoable actions remain When the user performs a new paint stroke after any undo Then the Redo stack is cleared
Atomicity of Mask Operations
Given an active editing session on an image When the user performs a continuous brush stroke (mousedown, drag, mouseup) Then the stroke is recorded as a single atomic history step When the user drags a mask to reorder layers and releases Then the entire reorder is recorded as a single atomic history step When the user adjusts a mask’s opacity via slider drag Then only the final value at mouseup is recorded as one atomic step (intermediate values are not recorded) And interruptions during an operation result in either the pre-action state or the complete action, never a partial state
Keyboard Shortcuts and Toolbar Controls
Given an image with at least one undoable action Then the Undo toolbar control is enabled and shows a tooltip "Undo (Ctrl/Cmd+Z)" And the Redo toolbar control is enabled only when there is at least one redoable action and shows a tooltip "Redo (Ctrl/Cmd+Shift+Z)" When the user presses Ctrl/Cmd+Z Then the most recent action is undone When the user presses Ctrl/Cmd+Shift+Z Then the most recently undone action is redone And the enabled/disabled state of both controls updates immediately to reflect availability
Session Persistence and Periodic Snapshots
Given an editing session has multiple history steps accumulated over at least 2 minutes And periodic snapshots are enabled at a default interval not exceeding 30 seconds When the app or browser is unexpectedly closed and the same image is reopened Then the canvas and masks restore to the last snapshot state And the undo/redo history is restored up to the snapshot point And no more than 30 seconds of work is lost between the last snapshot and the crash When editing resumes Then new history accumulates on top of the restored state and snapshots continue without duplication or corruption
Cross-Image History Isolation
Given two images (Image A and Image B) are open in the editor with their own masks and actions performed on each When the user focuses Image A and presses Undo Then only Image A’s history changes and Image B remains unchanged When the user switches focus to Image B and presses Redo Then only Image B’s history changes and Image A remains unchanged And no undo/redo on one image modifies the other image’s masks, order, opacity, or other properties
Multi-Mask Consistency Within One Image
Given an image with at least two masks (Mask 1 and Mask 2) And the user performs, in order: move Mask 1, change opacity of Mask 2, reorder Mask 1 below Mask 2 When the user presses Undo three times Then the state returns to exactly before these three actions, in reverse order, affecting only the masks that were acted upon When the user presses Redo three times Then the move, opacity change, and reorder are reapplied in the original sequence with identical positions and values And undoing an action on one mask does not alter another mask unless the operation explicitly involved both (e.g., reorder)
History Capacity Rollover
Given the history capacity per image is at least 50 steps When the user performs 70 atomic operations on a single image Then the history retains the most recent 50 operations and discards the oldest 20 When the user presses Undo repeatedly Then undo stops at the oldest retained step and the Undo control is disabled beyond that point When a new operation is performed after any undo Then the Redo stack is cleared and capacity rules continue to apply
Before/After Compare View
"As a board member, I want a clear before/after view so that I can verify the accuracy of edits and approve them confidently."
Description

Add an interactive compare mode to assess edits against the original: overlay toggle, opacity slider, and split-view slider; support side-by-side on larger screens and maintain synchronized zoom/pan. Ensure high performance for large images, accessible controls, and responsive layouts across web and mobile. Compare view is non-destructive and available anywhere the media editor is embedded within Duesly.

Acceptance Criteria
Overlay Toggle: Instant Before/After
- Given an edited image is open in the media editor, when the user activates Compare via the overlay toggle (pointer/touch or documented keyboard shortcut), then the view switches between Edited and Original in under 100 ms and the edit history remains unchanged. - Given Compare mode is active, when the user toggles the overlay off, then the editor returns to the prior edit view preserving tool state, active mask, and selections. - Given the image and edits are already loaded, when the user toggles overlay repeatedly 10 times, then no additional network requests are made and no errors are logged. - Given any compare action, then application state (undo/redo stack length, current step pointer) is unchanged before and after the action.
Opacity Blend Slider: Adjustable Before/After Mix
- Given Compare mode is active, when the user adjusts the opacity slider from 0% to 100%, then the blend updates continuously with keyboard step granularity of 1% and pointer/touch drag support. - Given the slider is at 0%, then only the Original is visible; given it is at 100%, then only the Edited is visible; given it is at 50%, then Original and Edited contribute equally within a tolerance of ±1%. - Given continuous adjustment, then the UI renders updates at ≥60 Hz with input-to-paint latency ≤120 ms on supported devices. - Given the user leaves and re-enters Compare for the same image in the same session, then the last-set opacity value is restored.
Split-View Wipe Slider: Edge-Aligned Compare
- Given Compare mode is active, when the user enables Split View, then a vertical draggable divider appears and the left side shows Original while the right side shows Edited by default. - Given the divider has focus, when the user presses Left/Right arrows, then the divider moves by 1% per keypress (10% with Shift) and remains within 0–100% bounds. - Given the user drags the divider, then both sides remain perfectly edge-aligned with no visible seam or misregistration (≤1 px) at any zoom level. - Given the divider handle, then its hit target is at least 44×44 px, it has a visible focus indicator, and double-click/double-tap resets position to 50%.
Responsive Compare Modes Across Viewports
- Given viewport width ≥1024 px, when the user selects Side-by-Side, then two panes render (Original left, Edited right) with synchronized zoom/pan; interactions in one pane apply to the other within 50 ms and zoom parity error ≤0.5%. - Given viewport width <1024 px, then Side-by-Side control is hidden or disabled, and Overlay and Split modes remain available. - Given the viewport is resized across the 1024 px threshold, then the compare mode gracefully switches to the supported mode while preserving zoom, pan, and compare settings. - Given mobile or touch devices, then all compare controls have minimum 44×44 px hit targets and labels are not truncated in portrait or landscape, respecting safe-area insets.
Accessibility: Keyboard and Screen Reader Support
- Given the editor is focused, when the user navigates via keyboard, then all compare controls are reachable in a logical tab order and have visible focus states. - Given a screen reader is active, then each compare control exposes an accessible name, role, and value (including current opacity percentage and split position) and announces mode changes (e.g., "Compare on: Overlay"). - Given keyboard-only usage, when the user invokes compare actions via documented shortcuts, then they can toggle compare, cycle modes, adjust opacity, and move the split divider without a pointer. - Given user prefers-reduced-motion, then non-essential compare animations are minimized or disabled while preserving functionality, and all content remains perceivable (WCAG 2.2 AA).
Availability and Non-Destructive Behavior Across Embeds
- Given the media editor is embedded in Post Composer, Compliance Case, Payment Attachment, or Announcement, then Compare entry points are present and functional in each context. - Given compare interactions (toggle, opacity, split, side-by-side), then no underlying image pixels, masks, or annotations are modified and the undo/redo stack length is unchanged before and after using compare. - Given the user exits Compare, then the editor restores the prior tool, active mask, and per-mask notes exactly as before. - Given a read-only embed (e.g., audit view), then Compare is available as view-only and does not expose edit tools or alter state.
Performance at Scale for Large Images
- Given an image up to 8000 px on the long edge with multiple active masks, when Compare is first activated, then the compare UI becomes interactive within 300 ms and remains responsive to input within 100 ms. - Given continuous pan/zoom in any multi-pane compare mode, then average input-to-frame latency is ≤120 ms and dropped frames are <5% over a 10-second interaction. - Given switching between Overlay, Split, and Side-by-Side, then the transition completes within 200 ms without freezing the UI thread. - Given memory usage in multi-pane modes, then peak memory does not exceed 1.8× the single-view footprint; if thresholds are approached, the system downscales preview resolution gracefully and indicates reduced resolution to the user.
Per‑Mask Notes and Rule References
"As a part-time manager, I want to add notes and rule references to each mask so that reviewers understand the context of every highlighted area."
Description

Allow each mask to include structured metadata: title, free-text note, optional tags, and a link to relevant community rules. Capture author and timestamps automatically. Display notes inline in the editor and surface them in compliance case views, exports, and activity feeds to provide clear context. Validate and sanitize inputs, enforce length limits, and support mentions for accountability.

Acceptance Criteria
Create Mask Note with Metadata
Given an existing mask in Brush Assist When I enter a title (<= 80 characters), a note body (<= 2000 characters), optional tags (<= 5, each <= 20 characters), and an optional rule link And I click Save Then the note is persisted with fields: title, body, tags, rule_link, author_id, author_name, created_at, updated_at And author_id equals the current signed-in user And created_at and updated_at are stored in ISO 8601 UTC format And the saved note is immediately visible in the mask's note panel
Inline Display in Editor and Compare View
Given a mask with a saved note When I select the mask in the editor Then the note displays inline with title, body, tags, rule link, author, created_at, and updated_at And clicking the rule link opens it in a new browser tab with target=_blank and rel=noopener When I toggle Compare View Then notes for both compared masks are visible and correctly associated with their respective masks
Validation and Sanitization of Inputs
Given I exceed any length limit (title > 80, body > 2000, more than 5 tags, tag > 20) Then Save is prevented and inline errors specify the violated limit Given I enter a non-https URL in the rule link Then a validation error prompts me to enter a valid https URL Given I paste HTML or script content into title or body Then the content is stored and rendered as escaped plain text and no script executes Given a tag contains disallowed characters Then only letters, numbers, spaces, hyphens, and underscores are accepted and others are rejected with an inline error
Rule Link Attachment and Interaction
Given a valid https rule link is saved with the note When I view the note Then the link is rendered as a clickable hyperlink When I click the link Then it opens in a new tab and does not navigate the editor away And the link value is included wherever the note is surfaced (editor, case view, exports)
Mentions in Notes for Accountability
Given I type '@' followed by at least 2 characters in the note body Then an autocomplete list of community members appears filtered by the typed characters When I select a member Then the mention is inserted as '@DisplayName' and stored as a structured mention linked to that member And up to 10 mentions are allowed per note And saved mentions render as clickable links to the member's profile within Duesly
Surface Notes in Compliance Case Views, Exports, and Activity Feed
Given a mask with a note is associated with a compliance case When I open the case Then the note is displayed in the evidence section with title, body, tags, rule link, author, created_at, updated_at When I export the case as CSV and JSON Then the export includes fields: mask_id, note_title, note_body, tags (pipe-delimited), rule_link, author_name, author_id, created_at, updated_at When a note is added, edited, or removed Then an activity feed entry is created including action (added/edited/removed), mask reference, actor, and timestamp
Save, Export, and Case Attachment
"As a manager, I want to save and attach the edited image and mask data to a compliance case so that the documentation is complete and shareable with homeowners and the board."
Description

Persist edited images with associated masks in Duesly storage, versioned and recoverable. Support exporting flattened images (PNG/JPEG) and structured mask data (JSON/SVG paths) for downstream workflows. Provide one-click attachment of the edited media and mask metadata to compliance cases and announcements, generating thumbnails and updating the activity feed. Enforce permissions, include authorship and timestamps, and ensure idempotent saves to prevent duplicate records.

Acceptance Criteria
Versioned Save with Mask Association
Given an edited image with one or more masks and per‑mask notes, and the user has Save permission When the user clicks Save Then a new immutable version is created with version_number = previous_version_number + 1 And the raster image, all mask geometries (paths), per‑mask notes, and related edit metadata are persisted atomically And the record includes author_user_id, created_at (UTC ISO‑8601), and edit_session_id And loading the image returns the latest version by default and allows selecting any prior version And restoring a prior version creates a new head version with pixels and masks identical to the restored version
Idempotent Save and Duplicate Prevention
Given a client provides an idempotency_key and an unchanged image/mask payload When the same Save request is retried one or more times within 24 hours due to network timeouts Then exactly one version record is stored And all subsequent retries return HTTP 200 with the original version_id and no additional versions or attachments are created And audit logs show a single save event for that idempotency_key And retries with a different payload and the same idempotency_key are rejected with HTTP 409 Conflict
Export Flattened Images (PNG/JPEG)
Given an edited image When the user exports as PNG Then the output matches the current canvas dimensions in pixels, uses sRGB IEC61966‑2.1 color profile, preserves transparency, and contains no visible mask overlays And the export completes within 5 seconds for images up to 6000x4000 pixels When the user exports as JPEG with quality Q (60–95, default 85) Then the output matches the current canvas dimensions, uses sRGB color space, has no transparency, and no visible mask overlays And a checksum (SHA‑256) is generated and stored with the export record
Export Structured Mask Data (JSON/SVG)
Given an edited image with N masks When the user exports mask data as JSON Then the file contains image_id, version_id, canvas_width_px, canvas_height_px, and an array of N masks with id, name, note, z_index, and geometry paths in pixel coordinates When the user exports mask data as SVG Then the SVG viewBox matches the canvas dimensions and contains N paths with IDs matching mask IDs and path data aligned to the top‑left origin And the count of masks in the export equals N, and a SHA‑256 checksum is stored with the export record
One‑Click Attachment to Cases and Announcements
Given a user with permission selects an edited image version and clicks Attach to Case or Attach to Announcement When the action is confirmed Then the flattened image and mask metadata are attached to the target record with links to the exact version_id And thumbnails at 64px, 256px, and 1024px longest edge are generated within 2 seconds and stored And the activity feed is updated with an entry including actor, timestamp (UTC ISO‑8601), target link, thumbnail, and action summary And repeating the action with the same version_id and target within 10 minutes is idempotent and does not create duplicate attachments
Permission Enforcement, Authorship, and Auditability
Given role‑based permissions are configured for Board Admin, Manager, Contributor, and Viewer When a Board Admin, Manager, or Contributor attempts Save, Export, or Attach Then authorized actions succeed with HTTP 200/201 and are logged with actor_user_id and timestamp When a Viewer attempts Save, Export, or Attach Then the action is blocked with HTTP 403 and no records or feed entries are created And all Save, Export, and Attach events are recorded in an immutable audit log retrievable by admins and filterable by actor_user_id, version_id, and date range

Bulk Redactor

Batch‑apply redaction to entire violation sets with consistent settings. Preview before publish, auto‑name files, and one‑click rollback if a change is needed. Portfolio managers clear backlogs in minutes while maintaining uniform privacy standards.

Requirements

Batch Scope Builder
"As a portfolio manager, I want to quickly define a batch of violations using flexible filters so that I can apply consistent redaction to all relevant items in one run."
Description

Enable selection of entire violation sets across one or multiple communities using filters (date range, community, rule type, status, tags), bulk checkboxes, and include/exclude controls. Supports saved scopes and immutable snapshots so the exact item list is locked at run time, ensuring repeatability and idempotent processing. Integrates with the violations index and respects existing data permissions and tenancy boundaries.

Acceptance Criteria
Filtered Multi-Community Scope
Given I have permission to multiple communities And the violations index contains records across those communities When I apply filters: date range, selected communities, rule types, statuses, and tags Then the scope results include only violations that match all selected filters And the total selected count equals the count returned by the violations index for the same filter set And clearing or changing any filter updates the results list and count to reflect the new filter state
Bulk Select All with Include/Exclude
Given paginated results with N total items matching the current filters When I click Select All Then N items are added to the selection across all pages And the selection count displays N When I manually deselect specific items Then those items are excluded from the selection and the count decreases accordingly When I re-select an excluded item Then it is included again and the count increases accordingly And exporting or snapshotting the scope uses the final inclusion/exclusion set
Save, Load, and Manage Scopes
Given an active filter and selection state (includes/excludes) When I save the scope with a unique name Then the scope persists with its filters and include/exclude rules And it is available in my Saved Scopes list When I load the saved scope later Then the filters and selection rules are reapplied to produce the current matching list And attempting to save with a duplicate name is rejected with a clear error When I delete a saved scope Then it is removed from my Saved Scopes list
Immutable Snapshot at Run Time
Given an active scope (saved or ad-hoc) When I initiate a bulk redaction run Then the system creates an immutable snapshot containing the exact list of violation IDs in the scope at run start And the run processes only the IDs in the snapshot even if underlying data changes mid-run And the snapshot stores timestamp, creator, item count, and ID list for audit When I retry the run using the same snapshot Then no additional items are processed beyond those in the snapshot (idempotent)
Integration with Violations Index Parity
Given the violations index supports filtering by date range, community, rule type, status, and tags When I build a scope using the same filter values in the Batch Scope Builder Then the results, counts, and sort order match the violations index And each listed violation links to its detail view in the index And any index-side permission or tenancy constraints are honored in the scope results
Permissions and Tenancy Enforcement
Given my user has access only to specific communities within a tenant When I build a scope Then only violations from permitted communities are visible and selectable And violations from other communities never appear in results or counts When I open a saved scope that references communities I lack access to Then those violations are excluded from view and selection with an access notification And snapshots and runs cannot include violations outside my current permissions
Redaction Preset Manager
"As a compliance lead, I want to create and enforce redaction presets so that every batch applies the same privacy policy without manual tweaking."
Description

Provide configurable, versioned redaction presets that define visual redaction behavior (boxes/blur, color/opacity, minimum font size), text pattern rules (PII keywords/regex), and media handling (EXIF/metadata stripping, output format, compression). Allow organization-level defaults and enforcement, with preset selection required at batch start to guarantee uniform privacy standards across the set.

Acceptance Criteria
Create and Version a Redaction Preset
Given I am an organization admin with Preset Manager permissions When I create a preset specifying visual settings, text pattern rules, and media handling options Then the system saves the preset as version 1.0 with a unique preset ID, semantic version number, creator, and timestamp Given an existing preset version 1.0 When I change any field and choose Save as new version Then a new version (e.g., 1.1) is created, the prior version remains immutable/read‑only, and a changelog records fields changed, editor, and timestamp Given a non-admin user When they view the preset list Then archived or draft preset versions are not selectable for batch use
Configure Visual Redaction Parameters
Given I set redaction style to boxes or blur, color as a hex value, opacity between 0% and 100%, and minimum font size in points When I save the preset Then the system validates inputs and rejects out-of-range values with field-level errors and prevents save Given a sample image is loaded in preview with the preset applied When I render the redaction overlay Then boxes/blur render according to configured style, color, and opacity, and the minimum font size constraint is enforced so redaction labels never render below the set size
Define PII Pattern Rules via Keywords and Regex
Given I add keyword lists and regex patterns with optional flags When I click Validate rules Then all regex compile without error, duplicates are flagged, and invalid patterns are rejected with inline error messages Given I enter sample text in the test panel When I run Test patterns Then all matching tokens are highlighted and a match count is displayed, and non-matching text is left unchanged Given I set a rule action to Redact or Ignore for each pattern When the preset is saved Then those actions persist and are applied consistently during batch processing
Apply Media Handling Policies on Export
Given a preset with EXIF/IPTC/XMP metadata stripping enabled and output format JPEG with quality 80% When assets are processed with this preset Then exported files contain no EXIF/IPTC/XMP metadata, are in JPEG format, and have compression quality 80% ±2% Given a preset with PDF output and vector redaction enabled When exporting a PDF Then underlying text and image data beneath redaction regions are irrecoverable (cannot be copied or searched) and the redaction objects are embedded per the preset
Set and Enforce Organization-Level Default Preset
Given I am an organization admin When I designate a preset version as the organization default Then it is stored as the default and is preselected for new batches for all users Given enforcement is enabled for approved presets When a user attempts to start a batch with a non-approved preset Then the selection is blocked and the user is shown a message indicating only approved presets may be used
Require Preset Selection at Batch Start
Given a user with permission to run bulk redaction When they attempt to start a batch without selecting a preset Then the Start action is disabled and API requests without preset_id are rejected with HTTP 400 and error code preset_required Given a user selects a preset version and starts the batch When items in the batch are processed Then the same preset version is applied to all items and the batch metadata records preset_id and preset_version; changing the preset mid-batch is disabled
Live Bulk Preview (Dry Run)
"As a community manager, I want to preview how redactions will look across the batch so that I can catch issues before publishing."
Description

Offer a pre-publish preview that renders side-by-side original vs. redacted for a representative sample and allows drilling into any item. Provide a full dry run mode that scans the entire scope, flags low-confidence detections or conflicts, and produces a readiness report before committing changes. Supports quick adjustments by switching presets without rebuilding the scope.

Acceptance Criteria
Side-by-Side Sample Preview Renders Correctly
Given a defined bulk violation scope and a selected redaction preset When the user initiates Live Bulk Preview Then the system displays a representative sample equal to 5% of the scope (min 20, max 200) as side-by-side Original vs Redacted pairs And each pair shows synchronized scroll and zoom, with redaction overlays and rule labels on the Redacted side And the sample is stratified to include at least one item per violation category present in the scope (when available) And no persistent records or files are altered during preview rendering
Drill-Down from Sample to Item Detail
Given the sample preview is displayed When the user selects any sample item Then an item-detail view opens showing Original and Redacted side-by-side with all redaction regions highlighted and confidence scores per region And the user can navigate back to the sample retaining scroll position and filters And the user can open the item’s full-resolution media within the detail view without committing any changes
Full-Scope Dry Run Readiness Report
Given a bulk violation scope and a selected redaction preset When the user runs Dry Run (Full Scan) Then the system analyzes 100% of items without writing changes to live data And a readiness report is generated containing: total items scanned; items ready to publish; items flagged low-confidence; items with conflicts; and counts per violation category And the report includes a sortable table of flagged items with reasons and links to item detail And the user can export the report as CSV and PDF including a dry run ID and timestamp
Low-Confidence and Conflict Flagging
Given a configurable confidence threshold for the active preset (default 0.85) When the dry run executes Then any item with one or more detections below the threshold is flagged Low Confidence with the lowest score displayed And conflicts caused by overlapping redactions covering >90% of the content area, mutually exclusive rule outcomes, or missing required fields are flagged Conflict with a specific reason code And these flag counts and reasons appear in the readiness report and on item-detail badges
Preset Switching Without Scope Rebuild
Given a completed dry run on a defined scope and cached analysis artifacts When the user switches to a different redaction preset while viewing the preview or item detail Then the system re-renders redactions using the existing scope without re-scanning or re-uploading items And the sample view updates within 2 seconds for samples up to 200 items, and item detail updates within 500 ms And the active preset name is clearly indicated in the UI header
Dry Run Performance and Non-Destructive Behavior
Given a dry run is initiated on scopes of varying sizes When the scope is ≤1,000 items Then the dry run completes in ≤60 seconds without writing any changes or triggering notifications When the scope is ≤5,000 items Then the dry run completes in ≤3 minutes without writing any changes or triggering notifications When the scope is ≤20,000 items Then the dry run completes in ≤12 minutes without writing any changes or triggering notifications And an audit log entry is recorded with user, scope size, preset, start/finish timestamps, and summary counts
Auto-Naming & Metadata Sanitization
"As an operations coordinator, I want files to be auto-named and sanitized so that outputs are consistent, private, and easy to find."
Description

Apply configurable file-naming templates with safe tokens (community, violation ID, date, preset version) and enforce PII-free names. Strip embedded metadata and thumbnails, normalize formats (PDF/PNG/JPG), and write outputs to a predictable storage path with unique, length-safe filenames. Integrates with Duesly’s document store and links back to the violation record automatically.

Acceptance Criteria
Apply Safe Token Naming Template to Batch Output
Given a naming template "{community}-{violationId}-{date:YYYYMMDD}-v{presetVersion}" And a batch of violations with known values When the Bulk Redactor generates outputs Then every output filename equals the template resolved using only the allowed tokens: {community}, {violationId}, {date:YYYYMMDD}, {presetVersion} And the {community} value is the configured community slug, not the display name And the date is derived from UTC and formatted as YYYYMMDD And filenames contain only [A-Za-z0-9-._] And spaces and other disallowed characters are normalized to "-" And multiple adjacent dashes are collapsed to a single dash And filenames are lower-cased consistently
Validate and Block Unsafe Tokens and PII in Naming Template
Given a naming template that includes any unsupported token (e.g., {unitOwnerName}) or literal text When template validation runs prior to processing Then validation fails with a clear error listing each unsupported token found And processing cannot start until the template is corrected And if the literal portion of the template or resolved values would introduce PII (e.g., email address pattern, 10+ digit phone number pattern), validation fails with a specific PII warning And the system logs the validation failure with the actor, timestamp, and template string
Strip All Embedded Metadata and Thumbnails
Given input files containing EXIF/IPTC/XMP metadata, GPS coordinates, embedded thumbnails, and PDF document info When sanitized outputs are produced Then all EXIF/IPTC/XMP fields are removed from PNG/JPG outputs And any GPS/geo tags are absent And PDF outputs have Info/XMP cleared (Title, Author, Subject, Keywords, Creator, Producer, CreationDate, ModDate, custom keys) And embedded thumbnails and file attachments are removed from outputs And running exiftool or equivalent on outputs returns no user or device-identifying metadata
Normalize Output Formats and Orientation
Given a preset that specifies a target format of PDF, PNG, or JPG And inputs of types PDF, PNG, JPG, HEIC, or TIFF When the batch is processed Then each output conforms to the preset target format And image outputs are converted to sRGB with 8-bit depth, with EXIF orientation applied and removed And JPG outputs contain no alpha channel and use the preset-defined quality/compression And multi-page inputs are preserved in order for PDF outputs And no output exceeds the preset-defined maximum dimensions or file size, if configured
Predictable Storage Path with Unique, Length‑Safe Filenames
Given a storage base path pattern "documents/{communityId}/violations/{violationId}/redactions/{YYYY}/{MM}/{DD}/" When outputs are written Then each file is stored under the computed path with the generated filename And if a filename collision occurs, a numeric suffix ("-1", "-2", ...) is appended before the extension to ensure uniqueness And the final filename length is <= 120 bytes and the full path length is <= 240 bytes And all writes are atomic and retried on transient errors, ensuring no partial files remain And a checksum (e.g., SHA-256) is stored alongside the document record for integrity verification
Auto-Link Documents to Violation Records in Document Store
Given a successful write of sanitized outputs to storage When document records are created in Duesly’s document store Then each record includes: docId, communityId, violationId, storagePath, filename, format, checksum, presetVersion, createdAt, createdBy And the associated violation record automatically lists these documents under its files/redactions section And the violation activity feed displays a "Redaction Published" event with links to each document And the public/board visibility of each document matches the violation’s permissions model And the API for GET /violations/{id} returns the new document IDs within documents.redactions
One-Click Rollback & Version History
"As a portfolio manager, I want to undo a batch with one click so that I can quickly correct mistakes without manual rework."
Description

Maintain versioned artifacts for each affected file and record, enabling rollback of an entire batch or selected items with a single action. Restore previous links and states without data loss, record actor/timestamp/reason, and block rollback when downstream dependencies would break unless explicitly overridden.

Acceptance Criteria
Rollback Entire Batch
Given a completed bulk redaction batch with versioned artifacts exists And the user has Manage Redactions permission When the user clicks "Rollback Batch" on the batch detail view and confirms with a non-empty reason Then the system reverts all files and records in the batch to their immediate previous versions And restores original filenames, metadata, visibility, and associations And records a rollback event with actor, timestamp, batch ID, and reason And produces a success summary showing total items attempted, succeeded, and failed And completes for batches up to 500 items within 120 seconds
Rollback Selected Items
Given a bulk redaction batch with versioned items exists When the user selects one or more items and clicks "Rollback Selected" and confirms with a reason Then only the selected items are reverted; non-selected items remain unchanged And each item rollback is atomic (success or no change) And per-item results and error messages are displayed and downloadable as CSV And the action is logged per item with actor, timestamp, and reason
Version History & Audit Trail
Given any file or record affected by Bulk Redactor When a user opens Version History Then entries show chronological versions including redaction and rollback events with version ID, actor, timestamp, action, and reason And a user can preview any prior version without altering the current version And the audit log for a batch can be exported as CSV and JSON And rolling back does not delete intermediate versions; all versions remain accessible
Dependency Check with Safe Override
Given a pending rollback (batch or selected items) When downstream dependencies would break (e.g., linked announcements, invoices/reminders, compliance cases, shared links) Then the system blocks the rollback by default and displays impacted entities and reasons And the user with Override Rollback permission may choose "Override and Proceed" after entering a reason And without override, no changes are applied And with override, the rollback proceeds and logs the override flag and impacted entities
State and Link Restoration Integrity
Given an item is rolled back Then its prior sharing state, access permissions, and feed placement are restored And previously issued URLs and references resolve to the restored version And references introduced by the redacted version are archived and not broken And a checksum/hash of the restored payload matches the stored prior version checksum
Rollback Confirmation and Impact Summary
Given a user initiates a rollback When the confirmation modal appears Then it lists the count of items to be reverted, item types, and any dependency blockers And the Confirm button is disabled until the user enters a non-empty reason And the user can view a detailed impact list before proceeding
Permissions and Concurrency Safety
Given role-based access control is enforced Then only users with Manage Redactions (and Override Rollback for overrides) see and can use rollback controls And when a target item has a newer version than the redacted one, the system prevents rollback of that item and displays a conflict message, offering "Create new version from prior state" if enabled And concurrent rollback attempts against the same batch are locked so only one executes at a time
Async Queueing & Progress Monitor
"As a part-time manager, I want bulk redaction to run in the background with clear progress so that I can keep working while the system processes."
Description

Process large batches asynchronously via a scalable worker queue with tenant-level throttling. Show real-time progress (completed/total, failures, ETA), allow pause/resume/retry of failed items, and publish results atomically after all items pass processing. Send completion and exception notifications via in-app alerts and email.

Acceptance Criteria
High-Volume Batch With Tenant-Level Throttling
Given a tenant with a configured concurrency limit C and a batch of N >= 5*C items enqueued When processing begins Then no more than C items from that tenant are active concurrently at any time And items from other tenants with available capacity begin within 1 second of their enqueue time And the system maintains at least 90% utilization of available worker slots across tenants while respecting per-tenant limits And per-tenant processing order is FIFO except for items explicitly paused or retried
Real-Time Progress Metrics and ETA Display
Given a batch is in progress When the user views the progress monitor Then the UI displays total, completed, in-progress, failed counts, percent complete, and an ETA And progress counts update within 2 seconds of backend state changes And ETA updates at least every 5 seconds while work is active And for batches with 100+ items, the final ETA differs from actual completion time by no more than ±20%
User-Initiated Pause and Resume
Given a batch is in progress When the user clicks Pause Then no new items are dequeued within 3 seconds And in-flight items are allowed to complete and are not aborted And the batch state displays Paused with counts frozen except for in-flight completions When the user clicks Resume Then dequeuing restarts within 3 seconds and progress updates resume without data loss
Retry Failed Items Individually or In Bulk
Given a batch has completed with K failed items When the user selects Retry All Failed Then all failed items move to Pending with attempt_count incremented by 1 And items exceeding the configured max_attempts are not retried and are clearly indicated And idempotency is enforced so retried items do not create duplicate artifacts or side effects When the user selects a subset of failed items and clicks Retry Then only the selected items are retried and their status transitions are logged
Atomic Publish Upon All Items Passing
Given a batch has processed all items successfully with zero failures When the user triggers Publish Results or auto-publish runs Then all redacted files and metadata are published in a single atomic operation with a single version identifier And no items are visible to end users prior to the atomic publish And if any item is failed or pending, the publish action is blocked with an explicit error And if a failure occurs during publish, no partial results are visible and the system rolls back to pre-publish state
Completion and Exception Notifications (In-App and Email)
Given a batch completes successfully When completion is recorded Then an in-app alert and an email are sent to the initiator (and configured recipients) within 60 seconds including batch name, total, succeeded, failed=0, duration, and a link to results Given a batch ends with one or more failed items after max retries When the batch enters a Failed state Then an in-app alert and an email are sent within 60 seconds including counts of failures, a summary of the first error, and a link to review/retry And duplicate notifications are not sent for the same terminal state
Access Control & Audit Logging
"As a board treasurer, I want permissions and audit logs around bulk redaction so that we meet our community’s privacy and compliance obligations."
Description

Restrict bulk redaction to authorized roles and optionally require a second approver for large batches or high-risk presets. Log all key events—scope creation, preset selection, preview, dry run results, publish, rollback—with immutable audit trails and exportable compliance reports.

Acceptance Criteria
Permission-Gated Access to Bulk Redactor
Given a user with the 'Bulk Redactor:Use' permission When they navigate to the Bulk Redactor Then the page loads and redaction controls are enabled Given a user without the 'Bulk Redactor:Use' permission When they navigate directly to the Bulk Redactor URL Then the request is rejected with HTTP 403 and no redaction action is performed Given an unauthorized user viewing posts in the feed When the UI renders action menus Then the Bulk Redactor action is not visible Given any attempt to access the Bulk Redactor (granted or denied) When the attempt is processed Then an audit event is recorded with actorId, outcome (granted|denied), timestamp (UTC), and requestId
Second Approver Required for High-Risk or Large Batches
Given a batch where (itemCount >= approverThreshold) OR the selected preset has riskLevel = 'high' When the creator attempts to publish Then the system requires a second approver before enabling Publish Given the creator selects a second approver When the approver is the same user as the creator Then the request is rejected with a message 'Approver must be a different user' Given a selected approver without 'Bulk Redactor:Approve' permission When the approval request is sent Then the request is blocked and an explanatory error is shown Given a valid approver receives a request When they approve within the approvalWindow (e.g., 72 hours) Then the batch transitions to Ready to Publish; otherwise the approval expires and publish remains disabled Given any approval action (requested, approved, declined, expired) When it occurs Then an audit event is recorded with batchId, creatorId, approverId, status, and timestamp (UTC)
Comprehensive Audit Events for Bulk Redaction Lifecycle
Given a user defines a batch scope When the scope is saved Then an audit event 'scope_created' is recorded with batchId, actorId, communityId, itemCount, and timestamp (UTC) Given a user selects a redaction preset When the preset is applied Then an audit event 'preset_selected' is recorded with presetId, presetVersion, and actorId Given a user triggers Preview When the preview completes Then an audit event 'preview_generated' is recorded with documentCount and redactionCount (no content stored) Given a user runs a Dry Run When it completes Then an audit event 'dry_run_result' is recorded with per-document counts and any warnings (no content stored) Given a user publishes a batch When publish succeeds Then an audit event 'publish_completed' is recorded with checksum/hash of output set and totals Given a user performs one-click rollback When rollback succeeds Then an audit event 'rollback_completed' is recorded with reason and restoredVersionId
Audit Log Immutability and Integrity
Given any audit event is written When a privileged user attempts to edit or delete it via UI or API Then the operation is blocked with HTTP 403 and an 'immutable_resource' error code Given the audit log service is queried for integrity When verifying the event chain Then each event exposes a SHA-256 checksum and previousHash so that tampering breaks verification Given read access to the audit log When fetching an event by id multiple times Then the payload is identical across requests and includes actorId, eventType, occurredAt (UTC), requestId, ipHash, and batchId (when applicable) Given any denied attempt to alter logs When it is blocked Then the denial itself is logged with actorId and timestamp
Exportable Compliance Report
Given a user with 'Audit:Export' permission When they request an export with date range, community filter, and eventType filters Then the system generates both CSV and JSON files containing all matching audit events and required fields Given an export is generated When the file is downloaded Then the filename is auto-named as 'audit_<communityId>_<fromUTC>_<toUTC>_<requestId>.csv|.json' Given an export is generated When validated Then each record includes eventId, eventType, occurredAt (UTC, ISO-8601), actorId, batchId, presetId, outcome/status, and checksum Given an export up to 50,000 events When requested Then the first byte is streamed within 5 seconds and the full file completes within 60 seconds in a standard staging environment Given an export is generated When verified Then a detached signature file (.sig) is provided to allow integrity validation
Preview and Dry Run Logging Without Sensitive Content
Given a user runs Preview or Dry Run When audit events are recorded Then no file contents, images, or unredacted text snippets are stored; only counts, document identifiers, and non-reversible hashes are recorded Given a test document containing PII When Preview and Dry Run are executed Then audit logs contain zero occurrences of the PII values (validated via redaction-safe keyword checks) Given logging is enabled When a developer enables debug mode Then audit payloads still exclude content fields and truncate oversized metadata according to limits (e.g., 4 KB per field)
Publish and One-Click Rollback Controls
Given a batch is in Ready to Publish state with all required approvals When a user with 'Bulk Redactor:Publish' permission confirms publish Then the publish completes atomically and records output counts and checksums Given a batch has been published When a user with 'Bulk Redactor:Rollback' permission triggers one-click rollback Then the system restores the pre-publish state for all items in the batch and disables duplicate rollback Given a rollback is executed When validation runs Then pre-publish and post-rollback counts of redactions and affected documents match exactly; discrepancies are flagged and rollback is marked failed Given a publish or rollback occurs When completed Then success or failure status and error details (if any) are recorded as audit events with timestamps and requestIds

Flex Split

Set household split rules by percent, fixed amount, or up‑to caps—plus optional minimums. Duesly applies the rules to every bill automatically so each co‑payer sees their exact share and the live remaining balance. Owners can allow co‑payers to propose adjustments for approval, with every change logged for clarity.

Requirements

Split Rule Builder & Validation
"As a primary household owner, I want to configure how our bill is split among co‑payers so that each person is charged fairly and consistently without manual math."
Description

Provide a rules editor for household owners to define co‑payer splits by percentage, fixed amount, up‑to caps, and optional minimums, with support for combining methods per co‑payer. Enforce validations (e.g., percent totals, conflicts between caps and minimums, negative outcomes) and allow selection of rounding strategy and precedence when caps/minimums intersect. Persist versioned rules at the household level, offer presets, and preview calculated outcomes against sample bills before saving. Integrates with Billing so saved rules auto-apply to all new and updated bills and are available via API and mobile/web UI.

Acceptance Criteria
Build Mixed Split Rules per Co‑payer
Given I am an owner in the Split Rule Builder for a household with at least two co‑payers When I configure a single co‑payer with a percentage (0.00–100.00%), a fixed amount (>= 0), an up‑to cap (>= 0), and an optional minimum (>= 0), and enable combining methods Then the builder accepts all inputs with currency precision to 2 decimals And it persists the configuration in the current draft without error And it displays the composed rule for that co‑payer as a single entry with all enabled components attached to that co‑payer And I can repeat this for each co‑payer in the household
Validation: Percent Totals, Caps/Minimums, Non‑negative Outcomes
Given a draft that uses only percentage allocations across co‑payers When I attempt to save with total percent not equal to 100.00% Then save is blocked and an inline error indicates the over/under amount to 2 decimal places Given any co‑payer has a minimum greater than its cap When I attempt to save Then save is blocked and the error identifies the affected co‑payer(s) Given a draft and a provided sample bill amount S > 0 When validation runs Then each co‑payer allocation is >= 0 And the sum of allocations is <= S (any remainder is reported as unallocated balance, not an error) Given all validations pass Then the Save action is enabled
Precedence and Rounding Application During Allocation
Given a draft includes fixed amounts, percentages, minimums, and caps across co‑payers And I select a precedence order (e.g., Fixed -> Minimums -> Percent -> Caps) And I select a rounding mode from {Nearest, Up, Down} to 2 decimal places When an allocation is computed for a sample bill amount S Then the engine applies the selected precedence strictly And no co‑payer allocation violates its minimum or exceeds its cap And rounding is applied per co‑payer using the selected mode And if caps do not limit the total, the sum of rounded allocations equals S via a deterministic remainder distribution And if caps limit the total, the sum of rounded allocations is <= S and the unallocated remainder is shown
Versioned Household‑level Rule Persistence and Audit Trail
Given a valid draft is saved Then a new version is created with a monotonically increasing version identifier, timestamp, and actor And the new version is marked Active for the household And version history lists prior versions as read‑only with diffs available And restoring a prior version creates a new Active version that duplicates its content without altering history And existing bills remain unchanged until edited, while new or edited bills use the current Active version
Preset Templates: Load, Modify, Save
Given I open the presets menu in the builder Then I can choose from at least: Equal Split, Owner Pays All, By Ownership Percentages, Primary Pays Remainder with Caps When I apply a preset Then the builder populates the fields accordingly and passes validation by default And I can modify any populated field and save as a new custom rule version And selecting Reset returns the form to the current Active version
Preview Calculated Outcomes on Sample Bills Before Save
Given a draft rule and a sample bill amount S are provided When I click Preview or change any rule input Then the per‑co‑payer allocations, any unallocated remainder, applied caps/minimums, and rounding effects are displayed And the previewed allocations match those produced when creating a test bill of amount S using the same rule version
Billing Integration and API/UI Availability
Given an Active rule version exists for a household When a new bill is created in that household Then allocations are automatically computed and shown in the bill details on web and mobile at creation time And when an existing bill is updated, allocations are recomputed using the current Active rule version And the API exposes the Active rule, rule history, and per‑bill allocations with the associated rule version identifier And allocation events are logged with the rule version reference for auditability
Auto-Apply Split to Bills
"As a board treasurer or manager, I want the system to auto-apply household split rules to each bill so that co‑payers see their exact share without manual intervention and totals always reconcile."
Description

Automatically compute and assign each co‑payer’s share for every bill (including recurring bills) using the current household split rules at bill creation and on bill edits. Recalculate shares on rule changes with safe reapplication (idempotent, with preview and confirmation) and support proration when bills are adjusted mid-cycle. Write allocations to a dedicated ledger and update each co‑payer’s bill view to show their exact owed amount and due date in the activity feed. Handles edge cases such as zero/negative line items and waived fees while maintaining total reconciliation to the bill amount.

Acceptance Criteria
Apply Split on New Bill Creation
Given a household with multiple co‑payers and active split rules (percent, fixed, caps, minimums) When a new bill is created with a total amount and due date Then the system computes each co‑payer’s allocation honoring the configured rules And the sum of all allocations equals the bill total to the cent with deterministic rounding And one allocation ledger entry per co‑payer is written with bill ID, co‑payer ID, amount, currency, rule version/hash, and timestamp And each co‑payer’s activity feed item for the bill displays their exact owed amount and the bill’s due date
Auto‑Apply Split on Recurring Bill Instances
Given a recurring bill template with a defined schedule When a new bill instance is generated from the template Then the current household split rules at generation time are applied to compute allocations And allocations are written to the allocation ledger referencing the template ID and instance ID And the sum of allocations equals the instance total to the cent And subsequent changes to split rules do not alter already‑generated instances unless a reapply is explicitly confirmed
Reapply Splits on Rule Change with Preview and Confirmation
Given existing bills with recorded allocations and a newer version of split rules When a user initiates reapplication for affected bills Then the system presents a preview showing for each bill and co‑payer: current allocation, proposed allocation, and delta, with all totals reconciling to the bill amount And no allocations are changed until the user confirms When the user confirms reapplication Then new allocation entries are written to the ledger as a new version and prior entries are marked superseded, preserving audit history And co‑payer views are updated to reflect the new owed amounts And performing reapply again without further rule changes results in no differences (idempotent)
Prorate Splits on Mid‑Cycle Bill Adjustment
Given a bill with existing allocations and potential partial payments When the bill total changes mid‑cycle due to line‑item edits or period adjustments Then the system prorates the delta across co‑payers based on the active split rules applied to the remaining balance only And previously recorded payments are not reallocated or reduced And allocation deltas are written to the ledger with references to the edit event and reason And rounding is deterministic and totals reconcile to the new bill amount And updated owed amounts and any due date changes are reflected in each co‑payer’s activity feed
Write Allocations to Ledger and Reconcile to Bill Total
Given any allocation event (initial apply, reapply, proration) When allocations are computed Then the write of bill updates and allocation ledger entries is atomic (no partial writes) And each ledger entry includes bill ID, co‑payer ID, amount (signed), currency, allocation version, computation method (apply/reapply/prorate), actor, timestamp And the sum of all co‑payer allocation amounts equals the bill’s net amount (including waivers and credits) And prior allocation versions remain readable for audit while being clearly marked as superseded
Update Co‑Payer Views with Owed Amount and Due Date
Given allocations exist for a bill When a user (co‑payer) views the bill in the activity feed Then the view displays the user’s exact owed amount, due date, and allocation status (e.g., applied, re‑applied, prorated) And after any allocation change (apply, reapply, proration), the view reflects the updated owed amount and due date without requiring page refresh And waived amounts and credits are clearly labeled and excluded from the displayed owed amount
Handle Zero/Negative Line Items and Waived Fees
Given a bill contains zero‑amount line items When allocations are computed Then zero‑amount items do not affect any co‑payer’s allocation and no errors occur Given a bill contains negative line items (credits) When allocations are computed Then credits are allocated according to the active rules without reducing any co‑payer’s owed amount below $0 unless the bill’s net total is negative And if the bill’s net total is negative, allocations sum to the negative total and are recorded as credits in the ledger, and co‑payer views indicate a credit Given fees are marked as waived When allocations are computed Then waived amounts are excluded from owed allocations while remaining visible in audit/ledger, and totals reconcile to the net bill amount
Real-time Balance Sync & Payment Allocation
"As a co‑payer, I want my owed amount to update immediately as others pay so that I always know my current responsibility and can plan my payments confidently."
Description

When a co‑payer makes a partial or full payment, update the remaining balances for all co‑payers in real time, honoring caps, minimums, and rounding policies. Define allocation rules for overpayments, underpayments, refunds, and chargebacks, and reflect reversals in both the allocation ledger and the UI. Surface a live remaining balance and activity timeline in each co‑payer’s bill card and in the household rollup, with push/email notifications for significant changes. Integrates with the Payments service and supports reconciliation back to the general ledger.

Acceptance Criteria
Real-time Balance Sync on Partial Payment
Given a household bill with co-payers and active split rules, and a successful partial payment event for co-payer A is received from the Payments service When the event is acknowledged by Duesly Then allocate the amount to A’s remaining obligation first according to allocation policy And update A’s remaining balance and the household remaining balance within 2 seconds And refresh all affected bill cards and the household rollup within 2 seconds And ensure sum of all co-payers’ remaining balances equals the household remaining balance (±$0.01)
Allocation Honors Caps, Minimums, and Rounding
Given split rules include percentage, fixed, caps, and minimums When computing allocations and applying any payment or reversal Then no co-payer’s allocated total exceeds their cap And no co-payer’s allocated total is below their minimum unless the bill total is insufficient And the sum of co-payer allocations equals the bill total with at most $0.01 rounding residual And assign any rounding residual to the co-payer with the largest fractional remainder; break ties by ascending co-payer ID And all balances display at 2-decimal precision
Overpayment Allocation and Household Credit
Given co-payer A pays more than their remaining obligation by amount E When processing the payment Then if "Redistribute overpayment" is OFF, record E as a household credit and do not reduce other co-payers’ remaining balances And show the household credit on the household rollup and timelines Then if "Redistribute overpayment" is ON, allocate E to other co-payers in priority order while respecting their caps and minimums And do not reduce any co-payer below $0.00 remaining And create ledger entries for each allocation recipient And update all affected balances and UIs within 2 seconds
Underpayment Application and Remaining Display
Given co-payer A pays less than their remaining obligation When applying the payment Then allocate the amount only to A’s remaining obligation And leave other co-payers’ remaining balances unchanged And update A’s bill card to show the new remaining amount and status And update the household remaining balance correspondingly within 2 seconds
Refunds and Chargebacks Reverse Allocations
Given a settled payment P with existing allocation entries When a refund or chargeback event for amount R tied to P is received from the Payments service Then post reversal ledger entries that mirror original allocations up to R And restore affected co-payers’ remaining balances accordingly And if a household credit was previously created from P, reverse the credit before increasing any individual’s remaining And annotate timelines with “Refunded” or “Charged back” including amount, date/time, and reference IDs And update all affected balances within 2 seconds and send push within 10 seconds; email within 5 minutes
Activity Timeline and Significant Change Notifications
Given any event (payment, redistribution, refund, chargeback, reversal) changes a co-payer’s remaining by at least $1.00 or sets status to Paid When the event is processed Then append a timeline entry with actor, event type, amount, before/after balances, allocation rule reference, and timestamp (UTC) And send push notifications to affected co-payers and household owner respecting notification preferences And send an email summary for significant changes within 5 minutes if email notifications are enabled And record delivery success/failure in notification logs
GL Reconciliation Integrity
Given ledger entries exist for allocations, reversals, credits, and debits with unique IDs and references to Payments transaction IDs When generating the GL export for a date range Then the sum of debits equals the sum of credits for that range And the export total matches Payments settlements net of configured fees And each Payments transaction ID has exactly one corresponding ledger root entry with any reversals linked by reference And re-running the export with the same parameters produces identical output (idempotent) And the reconciliation report shows zero unmatched or orphaned entries
Adjustment Proposal & Approval Workflow
"As a co‑payer, I want to propose an adjustment to my share and get owner approval so that we can handle exceptions without offline coordination or confusion."
Description

Allow co‑payers to propose temporary or permanent split adjustments on a bill or rule set, with optional reason, attachment, and proposed effective dates. Route proposals to the primary owner for approval or rejection, with inline discussion, auto-expiration windows, and clear status indicators; proposals do not affect balances until approved. On approval, automatically recalculate allocations and notify all parties; on rejection, log the decision and rationale. Provide admin overrides for managers where permitted by community policy settings.

Acceptance Criteria
Submit Proposal with Reason, Attachment, and Effective Dates
Given a co‑payer or owner with permission is viewing a bill or rule set with Flex Split active When they open "Propose Adjustment" and select Temporary or Permanent And they enter percent/fixed/cap/minimum changes within community policy limits And they optionally add a reason and a single attachment within permitted type/size limits And they set an effective start date (and end date for temporary) within allowed ranges Then the system validates all fields and blocks submission with inline errors for any violations And if another proposal is already pending for the same bill or rule set, submission is blocked with guidance to wait or withdraw the other proposal And upon successful submission, a proposal with status "Pending" is created with a unique ID And a preview of proposed allocations is displayed without altering current balances And the proposal creation event is written to the audit log with actor, timestamp, and payload summary
Route Proposal to Primary Owner with Notifications and Discussion
Given a proposal is created for a bill or rule set When the proposal status is set to "Pending" Then the primary owner is assigned as the approver And the primary owner receives an in‑app notification and an email containing proposal summary and a deep link And the bill/rule feed displays a visible "Pending Approval" badge with timestamp and proposer identity And an inline discussion thread tied to the proposal is available to involved parties (proposer, primary owner, permitted managers) And all comments and actions in the thread are time‑stamped and attributed
Approve Proposal Recalculates Allocations and Notifies Parties
Given a pending proposal with an effective date on or before today When the primary owner approves the proposal Then the system recalculates allocations according to the proposal details and policy constraints And updates each co‑payer’s share and the bill’s remaining balance accordingly And records prior vs. new allocations in the audit log with a diff And sets the proposal status to "Approved" And notifies all involved parties (proposer, co‑payers, primary owner, permitted managers) of the approval and new allocations
Reject Proposal Logs Rationale and Preserves Balances
Given a pending proposal When the primary owner rejects the proposal and enters a required rationale Then the system records the rejection and rationale in the audit log with actor and timestamp And sets the proposal status to "Rejected" And leaves all balances and allocations unchanged And notifies the proposer and involved parties of the rejection and rationale
Auto‑Expire Stale Proposals
Given a pending proposal older than the configured approval window When the window elapses without approval or rejection Then the system sets the proposal status to "Expired" And locks the proposal from further action (approve/reject/edit) And leaves balances and allocations unchanged And notifies the proposer and primary owner of the expiration And records the expiration event in the audit log
Admin Override per Community Policy
Given a manager with admin override permission per community policy views a pending or expired proposal When the manager attempts an override action (approve, reject, or force‑apply) Then the system enforces mandatory community constraints and blocks violations with clear errors And requires a rationale for any override And records the override action in the audit log as an admin override with actor identity and rationale And if force‑applied, updates allocations per the proposal and notifies all parties of the change
Scheduled Effective Date Activation and Cancellation
Given an approved proposal with an effective date in the future When the effective date/time is reached Then the system automatically applies the recalculation and updates allocations And sends activation notifications to all involved parties And until activation, balances remain unchanged and the proposal shows status "Scheduled" And before the effective date, the primary owner or an authorized manager can cancel activation And cancellation sets status to "Canceled", logs the action with rationale, and leaves balances unchanged
Split Calculation Audit Trail & Explainability
"As a household owner, I want a transparent history and explanation of how shares were calculated so that everyone trusts the numbers and disputes can be resolved quickly."
Description

Record a tamper-evident audit log of split rules, proposals, approvals, and allocation calculations, including who made changes, timestamps, previous values, and reasons. Provide an explain view that shows the exact formula and steps used to compute each co‑payer’s share (percent, fixed parts, caps, minimums, rounding) for any bill. Enable export of logs for dispute resolution and compliance, and surface concise explanations inline on bill views for transparency. Ensure logs are queryable via admin tools and respect role-based access controls.

Acceptance Criteria
Tamper-Evident Log on Split Rule Changes
Given a household with existing split rules and an authenticated Board Admin When the admin creates, updates, or deletes any split rule field (percent, fixed amount, caps, minimums, rounding mode) and provides a non-empty reason Then a log entry is persisted atomically with the change containing: id, entity_type=split_rule, entity_id, action (create|update|delete), actor_id, actor_role, household_id, timestamp (UTC ISO 8601), previous_values, new_values, reason, prev_hash, hash And the verify endpoint for the affected household returns chain_status=valid and last_hash matching the latest entry And any modification of a historical log entry causes the verify endpoint to return chain_status=broken at the modified entry And the API/UI exposes no mutation endpoints for log entries and attempts return 405 And if logging fails, the originating split rule change is rolled back and the user sees an error
Logged Proposals and Approvals for Split Adjustments
Given proposals are enabled by the owner for a household and a co‑payer is authenticated When the co‑payer submits a proposal to adjust their allocation with an optional reason Then a log entry is created with entity_type=proposal, action=create, proposed_deltas, actor_id, timestamp, correlation_id, prev_hash, hash And the owner is notified and the proposal status=Pending is visible with its log reference When the owner approves the proposal Then a log entry is created with entity_type=proposal, action=approve, approver_id, timestamp, correlation_id linking to the create entry And allocations are recalculated and a calculation log entry is created with entity_type=allocation, action=recalculate, inputs_snapshot and outputs_snapshot, correlation_id And duplicate approval attempts are rejected with 409 and no additional log entries are created When the owner rejects the proposal Then a log entry is created with entity_type=proposal, action=reject, reason, timestamp, correlation_id and no allocation change occurs
Explain View Shows Exact Allocation Formula and Steps
Given any bill with Flex Split rules that may include percent, fixed amounts, caps, minimums, and a rounding mode When an authorized user (Owner, Co‑payer for own share, Board Admin, Manager, Auditor) opens the Explain view for a co‑payer on that bill Then the view displays: bill_total, currency, rule components in order of application, intermediate subtotals after each component, applied caps and minimums with thresholds, rounding mode and precision, final share, and remaining balance handling method And each displayed number is shown with currency formatting for the user locale and full precision in a details tooltip And the final share matches the stored allocation value exactly and the sum of all co‑payers’ final shares equals bill_total And the view links each step to corresponding log entry ids (rule snapshot and calculation entry) And a machine-readable JSON of the formula and inputs/outputs is downloadable and matches the on-screen values
Inline Bill View Explanation Summary
Given a bill is visible in the feed to an authorized user When the bill renders each co‑payer row Then a concise summary string (≤120 characters) is shown per co‑payer explaining the share (e.g., "40% up to $200 + $25 min → $175.00") And tapping the info icon navigates to the Explain view for that co‑payer And the summary’s values and rounding match those in the Explain view and stored allocation And the summary respects locale/currency formatting and hides for users without permission to view that co‑payer’s details
Exportable Audit Logs with Filters and Integrity Check
Given an authenticated Board Admin or Auditor opens the Audit Log export tool When they select filters (date range UTC, community/household, bill id, actor id/role, action types, entity ids) and format (CSV or JSON) Then the system generates an export containing all matching log entries with fields: id, entity_type, entity_id, action, actor_id, actor_role, household_id, bill_id, timestamp (UTC ISO 8601), previous_values, new_values, reason, correlation_id, prev_hash, hash And the export includes a metadata header/trailer with generated_at (UTC), requested_by, applied_filters, record_count, and sha256 checksum And the checksum validates against the file contents and record_count matches the number of rows/objects And the export respects RBAC, redacting PII for roles without access to that data, and excludes entries the requester is not authorized to see And large exports are streamed in chunks with pagination cursors and do not time out for datasets up to 1 million rows
RBAC Enforcement and Admin Queryability of Logs
Given the admin log viewer UI or logs API is available When a user queries logs with filters (date range, actor, action type, entity type/id, correlation id, keyword in reason) and sort options Then results are returned paginated with stable cursors and total_count (or a capped estimate) and response time ≤1s for pages ≤100 records on typical load And Board Admins see only communities they administer; Managers see assigned communities; Owners see only their household’s logs; Co‑payers see only logs involving their allocations; Auditors with read-only scope can see permitted communities And unauthorized access attempts return 403 with no data leakage and are themselves logged with action=access_denied And filter and sort parameters are validated; invalid parameters return 400 with details and no partial results
Role-Based Access & Privacy Controls
"As a primary owner, I want strict access controls over who can view or change our split settings so that our household’s financial details remain private and secure."
Description

Enforce permissions so only primary owners can create/edit split rules, co‑payers can view rules and create proposals, and managers can view allocations without accessing co‑payer payment methods. Scope visibility so each co‑payer only sees their own owed amount, activity, and relevant explanations while the owner sees the full household view. Support community-level policy toggles (e.g., allow manager overrides), and ensure API endpoints and notifications respect these access rules. Log access events for security auditing and align with applicable privacy standards.

Acceptance Criteria
Owner-only split rule management permissions
Given a household with roles {primary owner, co-payer(s), manager} When the primary owner creates, edits, or deletes a Flex Split rule Then the operation succeeds and the new rules are applied to subsequent bill allocations And an access event is logged with actor, role, action, target household, and timestamp Given a co-payer or manager attempts to create, edit, or delete a Flex Split rule while community policy 'allow manager overrides' is OFF When the request is made via UI or API Then the operation is blocked with HTTP 403 (API) or disabled UI with rationale text And no changes are persisted And a denied-access event is logged
Co-payer restricted visibility of household data
Given a logged-in co-payer views a household bill and its Flex Split details When the page loads or API GET /households/{id}/allocations is called by that co-payer Then the response shows only the co-payer’s own owed amount, payment activity, and relevant explanations And the response excludes other co-payers’ names, emails, payment methods, amounts, and activity And navigating directly to another co-payer’s resource (e.g., /allocations/{otherId}) returns HTTP 403 and logs the attempt And UI elements for other co-payers are not rendered in the DOM
Manager read-only allocations without payment method access
Given a community manager opens a household’s Flex Split view while 'allow manager overrides' is OFF When the manager requests allocations via UI or API Then they can see each participant’s allocation amounts, status (paid/unpaid), and rule types And no payment method details (token, brand, last4, expiry, billing address) are returned or visible And edit controls for split rules are disabled/absent; API write endpoints return HTTP 403 And an access view event is logged without storing any sensitive payment method data
Co-payer adjustment proposals with owner approval and full audit trail
Given a co-payer wants to change their share When the co-payer submits a proposal with reason and a proposed percent/amount/cap Then the proposal is created with status=PENDING and references the current rule set And the primary owner receives a notification and can approve or decline When the owner approves Then allocations update accordingly, the proposal is marked APPROVED, and before/after values are recorded in the audit log When the owner declines Then no allocation changes occur and the proposal is marked DECLINED with an owner-visible reason And co-payers can only create proposals affecting their own share; attempts to modify others return HTTP 403 and are logged
Community policy toggle: allow manager overrides
Given a community admin toggles the 'allow manager overrides' policy When the policy is set to ON Then managers can create/edit/delete split rules successfully, with each change logged as a manager override including policy version When the policy is set to OFF Then manager attempts to edit split rules are blocked with HTTP 403 and explanatory UI text And policy changes propagate to authorization checks across UI and API within 60 seconds And each policy change is audit-logged with actor, timestamp, old→new value, and community ID
API and notifications enforce RBAC and data minimization
Given role-scoped tokens for primary owner, co-payer, and manager When calling Flex Split endpoints (e.g., GET/POST /split-rules, /allocations, /proposals) Then responses include only fields authorized for that role; unauthorized operations return HTTP 403 with error code RBAC_403 And manager responses never include any payment method fields; co-payers see only their own payment method metadata; owners see only masked details where applicable When notifications are sent for rule changes or proposals Then recipients are limited to authorized parties (e.g., owner and initiating co-payer) and content is redacted per role And subscription attempts to another user’s event stream are rejected with HTTP 403 and logged
Security audit logs and privacy controls compliance
Given any access or mutation to Flex Split resources When the action occurs via UI or API Then an immutable audit entry is created with event_id, actor_id, role, household_id, action, target_id, outcome, timestamp (UTC), ip, and user_agent And audit logs exclude sensitive payment data and store only masked identifiers for users where applicable And authorized reviewers (owner, designated community auditor) can filter/export audit logs (CSV/JSON) for a date range via API; others receive HTTP 403 And audit logs are retained for at least 18 months and are encrypted at rest; all transport uses TLS 1.2+

Backstop Autopay

Give owners a safety net: if a co‑payer hasn’t covered their share by the due date (or a grace window), Duesly auto‑runs an owner backstop for “whatever remains.” Receipts clearly show who paid which portion and when, preventing late fees and eliminating last‑minute chasing.

Requirements

Co-payer Setup & Split Allocation
"As an owner, I want to set up co-payers and specify each person’s share so that everyone knows their responsibility and any shortfall can be covered automatically."
Description

Enable owners and managers to add and manage co-payers on a unit or bill, define each co-payer’s share (percentage, fixed amount, or open contribution), and mark the bill as eligible for Backstop Autopay. Provides configurable defaults per community and per recurring charge, supports caps/minimums, and captures co-payer contact and payment preferences. Integrates with Duesly’s bill creation flow so any post-turned-bill can specify splits before publishing. Outcome: clear responsibilities for each participant and a structured basis for calculating any remaining balance at the end of the due period.

Acceptance Criteria
Create and save mixed split types on a single bill
Given a draft bill with a total amount is created And the bill creator adds three co-payers with split types: Percentage, Fixed Amount, and Open Contribution And the Percentage value is between 0 and 100 inclusive with up to two decimal places And the Fixed Amount is a non-negative currency value not exceeding the bill total When the creator clicks Save Splits Then the system persists each co-payer with their split type and value And the UI displays the computed dollar amount for each non-open split based on the bill total And the remaining amount is labeled as Open Contribution and equals Total − sum(non-open adjusted obligations) And no validation errors are shown
Validate allocation totals with and without Backstop eligibility
Given a draft bill with co-payer splits is ready to publish and the Backstop eligibility toggle is OFF When the creator clicks Publish Then the sum of fixed amounts plus percentage-based amounts (computed on the bill total) equals the bill total within $0.01 tolerance And if the sum is not equal, publish is blocked with an error: "Allocations must equal 100% of the bill when Backstop is off" Given the same bill and the Backstop eligibility toggle is ON When the creator clicks Publish Then the sum of fixed and percentage-based amounts may be less than the bill total And the difference is stored as Remaining Balance for potential backstop And if the sum exceeds the bill total by more than $0.01, publish is blocked with an error: "Allocations exceed bill total"
Apply caps and minimums to obligations
Given a draft bill with co-payers that include optional Minimum and Cap amounts When obligations are calculated Then each co-payer's raw obligation is computed from its split type (Percentage of total or Fixed Amount) And obligation = max(raw obligation, Minimum) when a Minimum is provided And obligation = min(obligation, Cap) when a Cap is provided And if Cap < Minimum for any co-payer, saving splits is blocked with error: "Cap must be greater than or equal to Minimum" And if sum(all Minimums) exceeds the bill total by more than $0.01, publishing is blocked with error: "Combined minimums exceed bill total" And Remaining Balance = Total − sum(adjusted obligations of non-open contribution co-payers) And all computed amounts are rounded to two decimal places using standard rounding (half up)
Capture co-payer contact and payment preferences
Given the Add Co-payer form is opened When the user enters First/Last Name or Organization Name and at least one of Email or Mobile Then the form validates that at least one contact method is present And Email is validated for format and Mobile is validated in E.164 format And the user can set Preferred Contact (Email or SMS) and Payment Preference (ACH, Card, None) And the user can toggle consent for automated reminders and autopay attempts And on Save, the co-payer record persists contact details, preferences, and consents And adding a duplicate co-payer (same email or mobile on the same bill) is blocked with an inline error
Apply default split templates at community and charge level
Given community-level default co-payer splits are configured And a recurring charge type has its own default co-payer splits When a bill is created from a post tagged with that charge type Then the charge-level defaults pre-populate the co-payers and their splits And any co-payers not specified at the charge level are merged in from community defaults And for overlapping co-payer roles, charge-level definitions override community defaults And the bill creator may edit or remove pre-populated entries without altering templates And a Restore Defaults action resets the bill to the applicable default set
Specify splits before publishing a post-turned-bill
Given a post is being converted to a bill When the creator reaches the Review & Publish step Then a Splits section is visible and editable And publishing is blocked until all validations for splits, caps/minimums, and allocation totals are satisfied per the Backstop setting And on successful publish, the bill record includes split definitions, adjusted obligations, and the Backstop eligibility flag
Edit co-payers and splits on a saved draft
Given a saved draft bill with existing co-payers and splits When the creator edits, adds, or removes co-payers Then the system recalculates obligations and remaining balance in real time And removing a co-payer reassigns its allocation to Remaining Balance (or requires redistribution if Backstop is OFF) And all validations re-run on each change And changes are persisted on Save Draft with version history retained for audit
Backstop Trigger & Execution Engine
"As a manager, I want the system to automatically charge the owner for any unpaid remainder at the end of the period so that late fees are avoided and I don’t have to chase payments."
Description

Run an automated charge for the owner’s designated Backstop method for the exact remaining amount when the due date (plus optional grace window) is reached. Supports per-community time zones, cut-off times, concurrency control, and queue-based processing to ensure reliable execution at scale. Includes configurable retry policies and failure handling without duplicate charges. Integrates with Duesly’s billing scheduler and payment processor to initiate transactions and update the ledger atomically.

Acceptance Criteria
Time-Zone-Aware Trigger with Grace and Cut-off
Given a community configured with a time zone (e.g., America/Denver), a bill due date/time, a grace window, and a daily cut-off time When the local time reaches the first cut-off at or after (due date/time + grace window) in that community time zone Then the engine enqueues a backstop charge job within 60 seconds of that cut-off And the job payload includes the computed trigger timestamp and community time zone identifier And no backstop job is enqueued before that cut-off for the bill
Exact Remaining Amount Backstop Charge
Given a bill with a total amount and a ledger reflecting all payments applied up to the moment of execution When the backstop charge executes Then the charge amount equals the bill balance (total minus posted payments/credits) rounded to the currency’s minor unit And if the balance is less than or equal to 0, no charge request is sent to the payment processor And the charge request includes bill ID, owner backstop method ID, computed amount, and an idempotency key
Queue-Based Scalable Execution
Given 10,000 eligible backstop jobs become triggerable within the same minute When the engine enqueues them to the processing queue Then per-bill FIFO ordering is preserved (no job for a bill starts before its predecessor finishes or is cancelled) And 95% of jobs are submitted to the payment processor within 5 minutes of enqueue time under nominal load And each job records enqueue and start timestamps for SLA verification
Concurrency Control and Duplicate Prevention
Given multiple workers may attempt to process the same backstop job due to retries or redelivery When processing begins for a job Then a distributed lock or claim token ensures only one worker proceeds at a time for that bill And the payment processor call uses a stable idempotency key derived from bill ID and charge cycle And at most one successful payment ledger entry exists for that bill/cycle; additional attempts receive idempotent confirmation without an extra charge
Retry Policy and Failure Handling
Given a charge attempt fails with a transient error (e.g., network timeout, 5xx from processor) When the engine handles the failure Then it retries up to a configurable max (default 3 attempts) with exponential backoff (starting at 2 minutes, capped at 30 minutes) And no more than one net successful charge occurs across retries Given a charge attempt fails with a permanent error (e.g., hard decline, invalid credentials) When the engine handles the failure Then it stops retrying, marks the backstop attempt as failed, and records a detailed failure event with error code and correlation ID And no payment ledger entry is created for the failed attempt
Atomic Ledger Update with Processor Events
Given the payment processor returns a successful authorization and capture for a backstop charge When the engine records the result Then a single atomic ledger transaction is written that includes the processor transaction ID, payer (owner backstop), amount, and timestamp, and reduces the bill balance accordingly And the ledger write and processor acknowledgement are idempotent with the same idempotency key Given the processor returns a failure When the engine records the result Then no payment entry is added to the ledger; only an audit log entry is created with failure details
Billing Scheduler Changes and Job Rescheduling
Given a scheduled backstop job exists for a bill When the bill’s due date, grace window, time zone, or cut-off configuration changes before the trigger time Then the engine cancels the existing job and schedules a new job at the recalculated trigger time And if the bill becomes fully paid before execution, the queued job is cancelled and no processor call is made And all schedule changes are logged with previous and new trigger times
Payment Method Vaulting & Backstop Authorization
"As an owner, I want to securely authorize a default payment method to cover any remaining balance so that I can prevent late fees without manually intervening."
Description

Provide secure storage and selection of the owner’s Backstop payment method(s) with explicit consent for “pay whatever remains” up to a configurable cap. Supports cards and ACH with PCI/NACHA-compliant vaulting, mandate capture, and audit of consent text, timestamp, and IP/device. Allows owners to prioritize fallback methods and set per-bill or global caps. Integrates with existing Duesly payment profiles and respects community-level fee settings.

Acceptance Criteria
Explicit Backstop Consent Capture & Audit
Given an owner enables Backstop on their profile, When they set a cap amount and select a payment method, Then the UI must present consent text that clearly states pay whatever remains up to the cap and require explicit opt-in before enabling Given consent is confirmed, When saving the configuration, Then the system stores consent_text_version, consent_timestamp (UTC ISO 8601), ip_address, device_user_agent, owner_id, payment_method_token, ach_mandate_id if applicable, and a hash of the displayed consent text in an immutable audit record retrievable by authorized users Given the owner cancels or does not opt in, When attempting to enable Backstop, Then Backstop remains disabled and no authorization record is created
PCI and NACHA Compliant Vaulting
Given an owner adds a card or bank account for Backstop, When submission occurs, Then sensitive fields do not transit or persist on Duesly servers and only a provider token is stored with masked display data (card brand, last4, expiration; or bank name, account type, last4) Given ACH is selected, When authorization is captured, Then a NACHA compliant mandate with owner name, company name, revocation instructions, and date/time is presented and mandate_id and timestamp are stored and linked to the payment method Given a payment method already exists in the owner payment profile, When selected for Backstop, Then the same token is referenced without creating duplicate vaulted records and is enabled for recurring backstop transactions
Method Prioritization and Automated Fallback
Given an owner has ordered multiple backstop methods, When a backstop charge executes, Then the system attempts the primary method first and logs attempt timestamp and gateway result code Given the primary attempt fails with a hard decline or NOC, When processing, Then the next prioritized method is attempted immediately and the sequence continues until a success or all methods fail Given the primary attempt fails with a soft decline or insufficient funds, When processing, Then the system retries per configured retry policy before falling back to the next method and all retries and outcomes are logged Given all methods fail, When processing is complete, Then the owner and board receive a failure notification and no further attempts occur until the owner updates methods or cap
Per Bill and Global Backstop Caps
Given a global cap and optional per bill cap are configured, When computing a backstop charge, Then the amount equals the minimum of remaining_balance_with_applicable_fees, per_bill_cap if set, and remaining_global_cap within the configured cap window Given community settings exclude fees from cap calculations, When charging, Then fees are added on top of the capped principal and the principal portion never exceeds the cap Given multiple bills trigger within the cap window, When successive backstop charges post, Then the remaining_global_cap is decremented accordingly and further charges that would exceed the cap are skipped with a notification to the owner
Integration With Payment Profiles and Community Fee Rules
Given an owner has existing vaulted payment methods, When enabling Backstop, Then the owner can select from existing tokens without reentry and selections are synced to their payment profile Given community fee settings define allowed methods and fee payer, When configuring Backstop, Then disallowed methods are not selectable and the estimated maximum charge clearly reflects whether fees are paid by owner or HOA and matches the eventual receipt Given fee surcharges or discounts apply, When a backstop charge is executed, Then the calculation uses the same fee rules as standard payments and the total charged respects cap rules as configured
Authorization Management and Revocation
Given Backstop is enabled, When the owner increases the cap or the consent text version changes, Then the owner must reconfirm consent and a new immutable audit entry is created upon confirmation Given the owner revokes Backstop authorization, When saved, Then all future backstop executions are disabled, any pending preauthorizations are voided if possible, an audit record is stored, and the owner receives confirmation Given an admin or owner views the profile, When accessing authorization history, Then consent audit records are visible with timestamps and identifiers but cannot be edited
Receipt Transparency and Attribution
Given a backstop payment is processed, When generating the receipt, Then the receipt itemizes each co payer contribution with timestamp, the backstop amount, fees, cap applied indicator, selected method nickname and last4, and a reference to the consent or mandate ID Given a partial remaining balance is covered by Backstop, When the receipt is delivered, Then it shows remaining balance as zero and clearly attributes who paid which portion and when, and the receipt is available to the owner and board within 1 minute of authorization or settlement event as configured
Remaining Balance Calculator & Idempotency
"As a treasurer, I want the remaining balance to be computed accurately and charged only once so that our books stay correct and residents are never double-charged."
Description

Calculate the precise remainder at run time by considering posted payments, credits, discounts, waivers, and fees, excluding pending/failed transactions. Handle rounding rules, partial payments, and adjustments made near the cut-off. Provide idempotency keys and transactional locks so that only one Backstop charge can occur per bill. Integrates with Duesly’s ledger to record the computed balance, the attempt, and the final settled amount consistently.

Acceptance Criteria
Compute Remainder at Run Time Using Posted Ledger Entries
Given a bill with a base due amount and ledger entries marked as posted, pending, or failed When Backstop Autopay evaluates the amount to charge at runtime Then it includes only posted payments, posted credits, posted discounts, posted waivers, and posted fees in the calculation And it excludes any pending or failed payments, credits, discounts, waivers, and fees And it computes remainder = max(0, round_to_minor_units(base_due + sum(posted_fees) - sum(posted_payments) - sum(posted_credits) - sum(posted_discounts) - sum(posted_waivers))) And if remainder > 0, it proceeds to attempt a backstop charge for exactly the computed remainder And if remainder = 0, no backstop charge attempt is created and the computation is recorded to the ledger
Currency Rounding, Non-Negative, and Zero-Amount Behavior
Given a currency C with minor unit precision p and rounding mode Half Up (unless otherwise configured per community) When the remainder is computed for a bill Then all arithmetic is performed in integer minor units to avoid floating-point error And the final remainder is rounded to p using the configured rounding mode And negative results are clamped to 0 And any computed amount less than one minor unit rounds to 0 And if the rounded remainder equals 0, no backstop attempt is created, and a 'Backstop Computed' ledger event is recorded
Honor Last-Minute Adjustments Near Cut-Off
Given a scheduled backstop run for a specific bill instance When the run starts, the system acquires a transactional lock on the bill and takes a consistent snapshot of posted ledger entries as-of snapshot_time Then any payments, credits, discounts, waivers, or fees posted at or before snapshot_time are included in the computation And any such entries posted after snapshot_time are excluded from that run and will be considered in future runs And the computed remainder and snapshot_time are recorded in the ledger for auditability And the same snapshot governs the attempt amount even if additional payments arrive before settlement completes
Single Backstop Charge per Bill via Locking and Idempotency
Given multiple concurrent triggers (jobs, API calls, or webhooks) to run Backstop for the same bill instance When they attempt to create a backstop charge Then a transactional lock on the bill instance ensures only one process creates an attempt And a deterministic idempotency key derived from the bill instance (e.g., backstop:{bill_instance_id}) is used for the attempt And at most one backstop attempt exists in Initiated/Succeeded states per bill instance And subsequent concurrent calls return the existing attempt (id, amount, status) without creating a new charge or additional authorizations
Idempotent Retries and Duplicate Notifications Do Not Double-Charge
Given network retries or timeouts cause the client or job to retry with the same idempotency key within the retention window When the system receives a duplicate create-charge request or duplicate payment provider webhooks Then it returns the original attempt and charge outcome without creating an additional attempt or authorization And the payment provider is invoked at most once per bill instance per idempotency key And the audit log shows a single attempt and a single settlement (or failure) for the idempotency key
Ledger Integration for Computed, Attempted, and Settled Amounts
Given Backstop computes a remainder for a bill instance When it records the event in the ledger Then it creates a 'Backstop Computed' entry containing bill_instance_id, snapshot_time, computed_amount, currency, and idempotency_key And if computed_amount > 0 and an attempt is created, it records a 'Backstop Attempted' entry with attempted_amount and reference to the payment intent/charge And on successful settlement, it records a posted payment entry with settled_amount linked to the attempt; on failure, it records a 'Backstop Failed' entry and no posted payment And all entries maintain referential integrity (foreign keys) and immutable audit fields (created_at, actor, source) And the sum of posted payments for the attempt equals the settled_amount, and ledger balances reconcile without drift
Itemized Receipts & Transparent Ledger
"As a co-payer, I want a receipt that shows everyone’s contributions and the Backstop amount so that there’s no confusion about who paid what."
Description

Generate receipts that clearly itemize who paid which portion and when, including the owner’s Backstop payment if triggered. Show masked payment methods, timestamps, and links to the underlying bill. Post receipts to the feed, email copies to participants, and store entries in the ledger for export/reporting. Access is permissioned so owners and co-payers see relevant details while managers see full audit information.

Acceptance Criteria
Backstop Receipt Itemization Accuracy
Given a bill totaling $X with owner A and co-payers B and C And B pays $Y before the due date And C pays $Z before the due date And at grace end the remaining balance R = X - (Y + Z) > 0 When Backstop Autopay runs for owner A Then a single receipt is generated for the bill with itemized lines for B ($Y), C ($Z), and "Backstop Autopay (Owner A)" ($R) And each line shows payer display name, role, masked payment method, and timestamps in community timezone and UTC And the sum of line amounts equals $X with no remainder And the receipt includes a working link to the underlying bill And if R = 0, the receipt contains no Backstop line and still sums to $X
Masked Payment Methods Display
Given a receipt contains card and ACH payments When the receipt is rendered in the feed, email, and export Then each payment line displays a masked method string And cards show "<Brand> •••• <last4>" (e.g., "Visa •••• 1234") And ACH shows "ACH •••• <last4>" And no full card number, full bank account, or routing number is displayed or retrievable And the same masked string appears identically in the ledger entry
Receipt Posting and Notifications
Given a receipt is generated for a bill with Backstop Autopay enabled When any settlement event occurs that completes or updates the bill (co-payer payment or backstop run) Then the latest receipt version is posted once to the community feed with a stable URL And email copies are sent to the owner and all co-payers on the bill within 60 seconds And each email includes the bill title, due date, itemized amounts, and a secure link to the receipt And the link requires authenticated access and enforces permission rules And delivery status (sent, bounced) is recorded in the audit log
Ledger Entry Creation and Export
Given a receipt exists for a bill When ledger entries are written Then one header entry references bill_id, receipt_id, and total amount And one detail entry is created per payment line (including Backstop) with payer_id, payer_role, amount, masked_method, timestamps (TZ and UTC), and link_to_receipt And the sum of detail amounts equals the header total And exporting the ledger for the relevant date range produces rows matching these entries in CSV with consistent column order and formatting And amounts and timestamps in the export match the receipt
Permissioned Access: Owners and Co-payers
Given a posted receipt for a unit with an owner and two co-payers When the owner views the receipt Then the owner sees full itemization including each payer's name, amount, masked method, and timestamps When a co-payer views the same receipt Then the co-payer sees their own payment line with full details and sees other payers as aggregated "Other contributions" with amounts hidden And the co-payer sees the Backstop line amount and payer role ("Owner") but not the owner's masked method details And an unaffiliated user cannot access the receipt URL
Manager Audit View Completeness
Given a manager views the receipt audit view When the manager opens the audit details Then all itemized lines show payer names, amounts, masked methods, and timestamps (TZ and UTC) And the audit timeline shows bill creation, reminders sent, payments settled, backstop triggered, emails sent/bounced, and ledger writes And links to the underlying bill and each payment transaction reference are present and functional And exporting the audit details yields a machine-readable file containing the same fields
Pre-run Alerts, Post-run Receipts, and Failure Notifications
"As an owner, I want timely alerts before and after Backstop runs so that I can avoid surprises and take action if something fails."
Description

Notify co-payers and the owner before Backstop runs (e.g., 24 hours before cut-off) with the current unpaid amount and a link to pay or pause Backstop. After execution, send success receipts; on failures, alert the owner and manager with next steps and retry timing. Support email, push, and in-feed notifications with configurable templates per community. Integrates with Duesly’s communications system and respects user notification preferences.

Acceptance Criteria
24‑Hour Pre‑Run Alert with Unpaid Amount and Action Links
Given a bill with multiple co‑payers and Backstop enabled for the owner and a community cut‑off time configured And the community has a pre‑run alert window set to 24 hours before cut‑off And at T = cut‑off − 24h the unpaid amount > $0.00 When the scheduler triggers the pre‑run alert Then the system sends a notification to each co‑payer and the owner via their allowed channels And the message includes the current unpaid amount calculated at send time And the message includes a deep link to pay (for co‑payers) and a deep link to pause Backstop (for the owner) And the event is recorded in the communications log with timestamps, channels, and recipient IDs And if the bill becomes fully paid before T, no pre‑run alert is sent And if partial payments post between scheduling and send time, the unpaid amount in the message reflects the latest ledger at send time
Owner Pauses Backstop from Pre‑Run Alert
Given the owner receives a pre‑run alert containing a pause Backstop deep link And the owner is authenticated or completes authentication When the owner confirms pause for the specific bill Then the bill’s Backstop status is set to Paused and the upcoming Backstop run is skipped And a confirmation notification is sent to the owner and reflected in the in‑feed And the pause action is logged with actor, timestamp, and bill ID And the pause link becomes single‑use and expires at cut‑off And unauthorized or expired link access returns an error and does not change Backstop state
Post‑Run Success Receipt with Contribution Breakdown
Given a Backstop run executes at cut‑off for a bill with remaining unpaid amount When the payment succeeds Then the system generates a receipt in the in‑feed and sends notifications per user preferences And the receipt shows a breakdown: each payer’s name (or masked identifier), amount paid, and timestamp, plus the owner Backstop amount And the sum of contributions equals the bill total and the remaining balance is $0.00 And the receipt includes the payment method descriptor for the Backstop portion and a unique receipt ID And the receipt is accessible to the owner, co‑payers, and manager according to permission rules And the receipt event is logged in the communications system with delivery outcomes per channel
Failure Notification with Next Steps and Retry Timing to Owner and Manager
Given a Backstop run attempts payment and the processor returns a failure When the failure is recorded Then the owner and the community manager receive failure notifications via their allowed channels And the notification includes the failure reason (if provided), a link to update payment method, and a support contact link And the notification displays the next retry timestamp and retries remaining based on the community’s retry policy And the failure event and scheduled retry are logged in the communications system And if a subsequent retry succeeds, a success receipt is sent; if final retry fails, a final failure notice is sent with recommended next steps
Channel Delivery and Preferences Respect (Email, Push, In‑Feed)
Given users have notification preferences set per channel for alerts, receipts, and failures When a pre‑run alert, success receipt, or failure event is triggered Then the system delivers via only the channels each user has allowed for that notification type (email, push, in‑feed) And no message is sent on channels the user has opted out of And delivery attempts, successes, and bounces are captured in the communications log with channel and status And invalid push tokens or bounced emails are marked and suppressed on subsequent sends to that channel for that user
Community‑Configurable Templates Render Correctly
Given a community has custom templates configured for email, push, and in‑feed for alerts, receipts, and failures with approved placeholders When a notification is generated Then the system renders the template with correct values (e.g., unpaid_amount, cut_off_time_local, retry_time, payer_breakdown) And if a community template for a channel/type is missing, the Duesly default template is used And templates validate allowed placeholders at save time and reject unknown variables And template changes apply only to future notifications and do not alter previously sent or posted messages
Scheduling Accuracy Across Time Zones and Grace Window
Given a community time zone and cut‑off time are configured and an optional grace window is set When scheduling the pre‑run alert and the Backstop execution Then all times are computed in the community’s local time, including during DST transitions And the pre‑run alert fires exactly 24 hours before the effective cut‑off (cut‑off + grace window if configured) And Backstop does not execute if the bill is fully paid before the effective cut‑off And no duplicate alerts or runs occur for the same bill cycle
Admin Controls, Overrides, and Late Fee Policy
"As a manager, I want controls to configure, pause, or override Backstop and align it with our late fee rules so that the feature reflects our community policies."
Description

Provide community-level settings to enable Backstop Autopay, define grace windows, set cut-off times, and configure late fee behavior (e.g., auto-suppress late fees if Backstop succeeds). Allow managers to pause, skip, or manually trigger Backstop for a bill, and to override splits when policy requires. Include audit logs for every change and a dashboard report of Backstop activity and outcomes. Integrates with billing policies and fee engines to ensure consistent enforcement.

Acceptance Criteria
Community-Level Toggle and Default Settings Saved
Given a community admin with permission to manage billing policies When they enable Backstop Autopay and set a default grace window (in whole days) and a daily cut-off time (HH:MM) in the community’s time zone Then the settings save successfully and persist across sessions And new bills created after the change inherit these defaults And existing bills do not change unless explicitly updated at the bill level And invalid values (negative days, non-time inputs, time outside 00:00–23:59) are rejected with inline validation errors And the current toggle state and values are visible in the settings UI and via the settings API
Grace Window and Cut-Off Time Enforcement
Given a bill with a due date, a configured grace window, and a cut-off time in the community’s time zone When the due date passes and the grace window elapses to the next configured cut-off Then Backstop Autopay evaluates the bill exactly at the cut-off time and only once per cycle And Backstop does not run before the cut-off or outside the community time zone And if the grace window is 0, Backstop evaluates at the first cut-off on the due date And if the outstanding balance is 0 at evaluation, Backstop does not run and records a “No Action – Paid” event
Late Fee Auto-Suppression When Backstop Succeeds
Given late fee policy is configured to suppress late fees when Backstop covers the remainder within the grace window And a bill reaches evaluation with an outstanding balance When Backstop successfully processes payment for whatever remains within the grace window Then no late fee is assessed by the fee engine for that bill And the bill ledger records a suppression reason tied to the Backstop transaction ID And if Backstop fails or is skipped and the balance remains, late fees are assessed per policy at the next eligible fee time
Manager Controls: Pause, Skip, and Manual Trigger
Given a bill eligible for Backstop and a manager with override permissions When the manager sets status to Pause Then Backstop does not evaluate or run for that bill until Resume is selected and saved When the manager selects Skip for the current cycle Then Backstop will not run for that cycle and will resume with the next scheduled cycle if applicable When the manager uses Manual Trigger Then Backstop attempts to charge exactly the current outstanding remainder once, uses an idempotency key, and respects payment method and policy constraints And Manual Trigger is disabled if outstanding remainder is 0 or Backstop is globally disabled for the community
Split Override With Policy Validation
Given a bill with multiple co-payers and default split rules When a manager overrides payer splits Then the new splits must sum to 100% with no negative allocations And a required reason must be entered and saved And the bill’s outstanding balances are recalculated immediately And Backstop’s target amount reflects the new remainder after override And receipts and reports reflect the updated split allocations and payer contributions
Audit Logging of Admin Actions and Backstop Events
Given audit logging is required for compliance When any admin changes Backstop settings, performs Pause/Skip/Manual Trigger, or overrides splits Then an immutable audit record is created capturing actor, timestamp (UTC), entity, action, before/after values, reason, and correlation IDs And Backstop run outcomes (scheduled, attempted, succeeded, failed, skipped, paused) are logged with amounts and payment method references And audit logs are retrievable via UI and API with filters by date range, action type, actor, and entity And audit records cannot be edited or deleted by end users
Dashboard Report of Backstop Activity and Outcomes
Given a manager opens the Backstop dashboard for a selected date range When the data loads Then the dashboard displays counts of scheduled, attempted, succeeded, failed, skipped, paused, and manually triggered runs; total recovered amount; number and value of late fees suppressed vs applied And supports filters by community, building/unit grouping, bill type/tag, and status And provides drill-down links to bills/receipts and export to CSV with column totals that reconcile to ledger and audit counts for the same range And data freshness SLA is displayed and the report updates within 15 minutes of new events

Nudge Sync

Coordinate reminders across co‑payers so the right person gets the right nudge at the right time. If one pays, Duesly quiets prompts for the rest; if neither pays, it escalates gently to both via their preferred channels. Less noise, fewer crossed wires, faster full payment.

Requirements

Co‑Payer Linking and Roles
"As a board treasurer, I want to link multiple co‑payers to a unit with clear roles so that reminders can target the right person without duplicating or confusing notices."
Description

Introduce a data model and UI to associate multiple payer profiles with a single unit or account, with configurable roles (e.g., primary, secondary, employer reimbursement) and relationship context. Support capture and management of each co‑payer’s contact details, preferred channels, and legal consents. Provide admin tools to add/remove co‑payers, set role precedence, and validate contact methods. Expose CRUD APIs for integrations. This foundation enables Nudge Sync to target the correct individual per invoice while maintaining a single source of truth for household responsibility.

Acceptance Criteria
Admin links multiple co‑payers to a unit and sets role precedence
Given an existing unit without co‑payers When an admin adds three co‑payers with roles Primary (precedence 1), Secondary (precedence 2), and Employer Reimbursement (precedence 3) Then the unit has exactly one Primary role And each co‑payer role has a unique precedence value with no gaps or duplicates And saving without a Primary role returns a validation error And assigning more than one Primary returns a validation error And removing the Primary without promoting another to Primary is blocked with an error And the co‑payer list for the unit displays the three co‑payers in precedence order
Contact method validation and verification for each co‑payer
Given a co‑payer profile in edit mode When the admin enters an email address and a mobile phone number Then the email must match RFC 5322 format and the phone must be in E.164 format And invalid inputs display inline errors and prevent save When the admin requests verification, an email magic link and an SMS one‑time code are sent Then following the magic link marks email as Verified with timestamp and source And entering the correct one‑time code marks phone as Verified with timestamp and source And unverified contact methods are labeled Unverified and cannot be set as preferred channels
Legal consent capture and enforcement for messaging channels
Given a co‑payer with email and SMS contact methods present When the admin records explicit consent for Email and SMS with timestamp, source, and scope Then the system stores consent records per channel with immutable audit metadata And preferred channels can only be set to methods with active consent When consent is withdrawn for SMS Then the preferred channel SMS is automatically unset and future SMS nudges are blocked And an audit log entry is created for the withdrawal
Admin adds and removes co‑payers with audit trail and permissions enforced
Given a user with Admin role on the community When they add a co‑payer to a unit Then the co‑payer is associated to the unit with a unique co‑payer ID and current effective date And an audit log records who performed the action, what changed, and when When the admin removes a co‑payer Then the association is soft‑deleted, the co‑payer becomes ineligible for nudges, and historical invoices/payments remain attributed And an audit log records the removal And users without Admin role cannot add or remove co‑payers and receive a 403 permission error
Integrations manage co‑payers via authenticated CRUD APIs
Given a valid OAuth2 access token with scope duesly.copayers.write When a POST /v1/units/{unitId}/copayers is sent with role, precedence, contacts, and consents Then the API responds 201 Created with the new co‑payer ID and persisted fields When a PATCH /v1/copayers/{id} updates role, precedence, contacts, or consents Then the API responds 200 OK and validations mirror the UI (e.g., exactly one Primary, verified contacts for preferred channels) When a DELETE /v1/copayers/{id} is sent Then the API responds 204 No Content and soft‑deletes the association And GET endpoints support filtering by unitId, role, and verification status and return 200 OK with pagination And write endpoints honor an Idempotency-Key header and return 409 Conflict on duplicate or conflicting precedence assignments
Unit profile displays co‑payers, roles, relationship context, and contact/consent status
Given a unit with multiple co‑payers and configured roles, relationships, and contact statuses When an admin opens the unit profile’s Co‑Payers tab Then the list shows each co‑payer’s name, role, precedence number, relationship (e.g., spouse, roommate, employer), and responsibility badges And contact methods display verification statuses and consent badges per channel And the list is ordered by precedence and supports drag‑and‑drop to change precedence with inline validation And changes persist on save and are immediately available via API and UI queries
Shared Invoice State and Quieting Logic
"As a property manager, I want reminders to stop for everyone once one co‑payer pays so that residents aren’t spammed and I avoid service complaints."
Description

Implement a shared invoice state machine across co‑payers that reacts in real time to payments, partial payments, promises to pay, disputes, and failures. When any co‑payer pays in full, automatically silence pending reminders for all others; when partial payments occur, update remaining balances and adapt who gets nudged next. Deduplicate reminders across channels and devices, enforce idempotency, and ensure reconciliation with payment processors to prevent double nudges after settlement.

Acceptance Criteria
Full Payment Silences All Co‑Payer Reminders
Given an invoice with multiple co‑payers has active scheduled reminders across email, SMS, and push And at least one reminder is queued for a non‑paying co‑payer When any co‑payer completes a payment that reduces the invoice remaining balance to 0 Then all queued and in‑flight reminders for all co‑payers on this invoice are canceled within 5 seconds of payment confirmation And no new reminders are scheduled for this invoice thereafter And only a payment receipt is sent to the paying co‑payer according to their notification preferences And the activity log records the quieting action with timestamp, paying co‑payer ID, and the IDs of canceled reminders
Partial Payment Reallocates Balance and Nudge Targeting
Given an invoice with at least two co‑payers has a remaining balance > 0 and scheduled reminders And each co‑payer has a tracked paid‑to‑date amount and optional target share When co‑payer A makes a partial payment P that reduces the remaining balance to R > 0 Then the system updates co‑payer A’s paid‑to‑date and the invoice remaining balance within 5 seconds of payment confirmation And scheduled reminders for co‑payer A are recalculated; if A’s target share is satisfied, suppress future nudges to A for this invoice And scheduled reminders for other co‑payers are updated to reflect the new remaining balance R and their unpaid share And the next nudge target is selected as the co‑payer with the largest unpaid share without an active promise or dispute And the activity log shows the recalculation, including old vs new balances and next nudge target
Promise‑to‑Pay Temporarily Suppresses Nudges and Reschedules
Given a co‑payer submits a promise‑to‑pay with a due date/time for an open invoice When the promise is recorded Then reminders to that co‑payer are suppressed until the earlier of the promise due datetime or full payment by any co‑payer And reminders for other co‑payers continue based on the remaining balance and their statuses And if the promise due datetime elapses without full payment by that co‑payer, an escalation nudge is scheduled within 15 minutes to that co‑payer with context of the missed promise And the activity log records promise creation, suppression window, and any breach/escalation events
Dispute Pauses Nudges for Disputing Co‑Payer Only
Given a co‑payer opens a dispute on an invoice and the dispute status is Under Review When the dispute is recorded Then reminders to the disputing co‑payer are paused immediately And reminders to non‑disputing co‑payers continue based on the remaining balance And if the dispute resolves with an adjustment or waiver, the invoice remaining balance and future nudge plan are recalculated within 60 seconds And if the dispute resolves against the co‑payer with balance still due, reminders to that co‑payer are reinstated with a minimum 1‑hour delay and include resolution context And all dispute status changes and nudge adjustments are captured in the activity log
Deduplicated, Idempotent Reminders Across Channels and Devices
Given the system generates a reminder for a specific invoice, co‑payer, and scheduled nudge slot When attempting delivery across multiple channels or devices Then no more than one reminder is delivered per co‑payer per nudge slot across all channels and devices And each send attempt uses a deterministic idempotency key of invoice_id + co_payer_id + nudge_slot + channel_group And repeated send attempts with the same key within 24 hours do not create additional deliveries And on channel fallback, failed attempts are marked failed and exactly one successful delivery is recorded And the activity log stores the idempotency key, channel outcomes, and final delivery status
Post‑Settlement Reconciliation Prevents Double Nudges
Given a payment authorization exists for an invoice and is pending settlement When a reconciliation event indicates settled, partial, or failed settlement Then reminders for the invoice are suppressed from authorization capture time until settlement status resolves And if settlement results in full payment, full‑payment quieting logic is applied and no reminders are sent after the settlement timestamp And if settlement is partial or fails, the remaining balance and nudge plan are recalculated within 60 seconds of the reconciliation event And duplicate or delayed reconciliation webhooks are handled idempotently by transaction_id and status so no extra reminders are sent
Failed Payment Reverts State and Escalates Nudges
Given a previously captured payment for an invoice is later failed, reversed, or charged back by the processor When the failure event is received Then the invoice remaining balance is restored accordingly within 60 seconds And reminders for co‑payers with outstanding shares are reinstated, excluding any co‑payer with an active dispute or valid promise window And an escalation nudge with failure context (non‑sensitive) is scheduled in the next nudge window to all responsible co‑payers via their preferred channels And the activity log records processor reference, reason code, restored balance, and the updated reminder schedule
Smart Reminder Orchestrator and Escalation Rules
"As a volunteer board member, I want the system to decide who to contact and when so that nudges are effective without me micromanaging reminders."
Description

Create a rules‑driven scheduler that selects the next recipient, message, channel, and timing based on role precedence, engagement signals (opens, clicks), payment history, overdue age, last contact, and HOA policy. Define escalation paths that gently expand outreach (e.g., from primary only to both co‑payers) when no progress is detected, with configurable cadence and caps. Provide reusable templates, per‑community policies, throttling, and backoff to deliver the right nudge to the right person at the right time.

Acceptance Criteria
Primary Payer First, Quiet on Payment
Given an invoice with two co-payers and role precedence (Primary > Secondary) and a policy requiring single-recipient initial nudges When a reminder is due Then the scheduler selects the Primary recipient, uses their preferred eligible channel, and schedules one message within the configured send window And the decision is logged with recipient_id, channel, template_id, reason_codes=[role_precedence, policy_single_recipient] Given payment in full is recorded by any co-payer When the payment is posted Then all future nudges for that invoice are canceled within policy latency (<=5 minutes) and suppression_reason='Paid in full' is logged Given a partial payment below the minimum-pay threshold When the next nudge is evaluated Then remaining balance is recalculated and nudges continue per policy with the updated amount displayed
Engagement-Driven Channel Switch
Given two consecutive unopened emails to a recipient within the policy lookback window When the next nudge is selected Then the channel switches to the next consented channel per channel_precedence and switch_reason='Low engagement' is logged Given no SMS consent on file When email is low engagement Then the system does not send SMS and instead selects the next eligible channel or defers if none Given a recipient clicked a nudge but no payment occurred within 48 hours When the next nudge is scheduled Then it uses a follow-up template variant for the same channel and includes last_click_ts in context
Escalation to Both Co-Payers After No Progress
Given no payment and no positive engagement (no opens or clicks) within the policy lookback window and overdue_age >= escalation_threshold When the next nudge is evaluated Then escalation_level increments and both co-payers are targeted subject to channel consent and caps And messages are scheduled within the configured escalation window and respect per-recipient cooldowns And an escalation event is logged with level, previous_level, reasons=[no_progress, overdue_age]
Cadence, Caps, and Throttling Enforcement
Given per-recipient cadence and max_nudges_per_cycle are configured When scheduling nudges Then the system ensures no more than D per day and M per billing cycle per recipient, enforcing channel-specific cooldowns Given a recipient has reached a cap When a nudge would be scheduled Then the nudge is suppressed, next_eligible_at is computed, and suppression_reason='Cap reached' is logged Given global send rate limits and provider error rate thresholds are configured When throughput or error rates exceed limits Then backoff is applied by the configured backoff_factor and retries are scheduled with exponential backoff up to N attempts
Per-Community Policy Selection and Overrides
Given a community with policy set P and an invoice in that community When the scheduler runs Then it uses the templates, channels, cadence, and thresholds from P and records policy_id in the decision log Given an invoice-level override for schedule or channel exists When selecting the next nudge Then override settings take precedence over community policy and are recorded with override_id Given a policy change is published When the scheduler next evaluates queued future nudges Then it re-evaluates and re-plans them within 10 minutes, honoring immutability for messages already sent
Template Selection, Personalization, and Audit Logging
Given a reminder stage, channel, and role When preparing a message Then the correct template variant is selected and content tokens resolve without missing or empty required fields Given any required token is missing When rendering Then the send is blocked, an error is logged, and a fallback notification is raised to admins; no message is sent Given a nudge is sent or suppressed When the action completes Then an audit record is created with fields [invoice_id, recipient_ids, channels, template_id, decision_factors, scheduled_at, sent_at/null, result=sent/suppressed/failed, reason] and is queryable within the admin UI
Bounce, Failure, and Opt-Out Backoff
Given a hard bounce (email) or carrier-invalid (SMS) response When future nudges are scheduled Then that channel is marked invalid for the recipient and will not be attempted again until re-validated Given a soft failure When retrying Then exponential backoff is applied with at most N retries within 72 hours; after exhausting retries, the attempt is marked failed and the next cadence window is awaited Given an opt-out or unsubscribe for a channel When selecting channels Then that channel is excluded and opt_out_reason is logged; if no eligible channels remain, the nudge is suppressed with reason='No eligible channel'
Channel Preferences, Quiet Hours, and Failover
"As a resident co‑payer, I want reminders to come via my preferred channel and at reasonable hours so that I notice them and don’t feel harassed."
Description

Honor per‑user channel preferences (email, SMS, push) with ranked order, local time zone quiet hours, and do‑not‑disturb windows. Detect delivery failures and auto‑failover to the next approved channel while respecting consent and rate limits. Support per‑community defaults and per‑invoice overrides. Ensure messages are scheduled in recipients’ local time, with safeguards for holidays and weekends to reduce noise and improve response.

Acceptance Criteria
Honor Ranked Channel Preferences and Consent per Recipient
Given recipient R has channel preferences ranked [push, SMS, email] and has consented to push and email, When a payment reminder is due, Then the system sends via push first, And logs delivery attempt with timestamp and message ID, And does not attempt SMS, And if push returns "device unreachable," Then the system schedules email according to quiet hours. Given recipient R has not consented to any channel allowed by community defaults or invoice overrides, When a reminder is due, Then the system does not send, And records status "blocked: no consent" visible in the activity log.
Enforce Quiet Hours and Do-Not-Disturb in Recipient Local Time
Given recipient R has quiet hours 21:00–08:00 and a recurring DND window 12:00–13:00 in America/Denver, When a reminder is scheduled for 22:15 local time, Then the system defers the send to 08:00 next business day local time, And if 08:00 falls within a configured holiday, Then the send is deferred to 09:00 next non-holiday business day. Given recipient R enables a one-time DND until 17:30 local time, When a reminder becomes due at 17:00, Then it is rescheduled to 17:30 local time.
Detect Delivery Failures and Auto-Failover Respecting Limits
Given an SMS send returns carrier status "undeliverable" or "blocked", When the failure is received, Then the system records the failure code and time, And automatically fails over to the next consented channel in the recipient’s ranked order within 60 seconds, respecting quiet hours and rate limits. Given a push notification returns a transient error, When the system retries up to 3 times within 5 minutes without success, Then it fails over to the next consented channel and records "failover: retries exhausted". Given an email soft bounce occurs, When three consecutive soft bounces occur within 24 hours, Then the system treats it as a failure and fails over to the next consented channel.
Apply Per-Community Defaults and Per-Invoice Overrides with Precedence
Given community defaults specify channels [email, SMS] and quiet hours 20:00–08:00, And an invoice override specifies channels [push] and quiet hours 18:00–09:00, When scheduling a reminder, Then the invoice override takes precedence for recipients who have push consent, And recipients without push consent fall back to community defaults, And all scheduling uses the recipient’s local time. Given both community defaults and invoice override define rate limits, When limits conflict, Then the more restrictive limit is applied. Given an invoice override removes SMS for a recipient who only consents to SMS, When attempting to send, Then the system does not send and records "blocked: no consent after override".
Schedule in Recipient Time Zone with Weekend and Holiday Safeguards
Given recipients R1 in America/New_York and R2 in Asia/Tokyo, When a reminder is scheduled for 10:00 recipient local time, Then R1 receives at 10:00 ET and R2 receives at 10:00 JST on their respective dates. Given a scheduled send falls on a weekend or configured holiday in the recipient’s locale, When scheduling occurs, Then the message is deferred to the next business day at the end of quiet hours (e.g., 08:00 if quiet hours end at 08:00). Given a DST spring-forward where 02:30 local time does not exist, When a reminder is scheduled for 02:30, Then it is sent at 03:00 local time, And in fall-back where 01:30 repeats, Then only a single delivery occurs with idempotency enforced.
Enforce Per-User, Per-Channel, and Community Rate Limits
Given configured limits of max 2 nudges per user per 24 hours and max 1 nudge per channel per user per 12 hours, When the scheduler attempts a third nudge within 24 hours, Then it is deferred to the next allowable time and the deferral is logged. Given a failover would violate the per-channel limit, When a primary channel fails, Then the system defers to the earliest time that satisfies both rate limits and quiet hours instead of sending immediately. Given a community-wide cap of 200 messages per hour, When the cap is reached, Then remaining messages are queued FIFO with fairness across recipients, And actual send times are adjusted but still comply with recipient quiet hours.
Consent, Audit Trail, and Compliance Guardrails
"As a Duesly admin, I want consent enforcement and full auditability of nudges so that we remain compliant and can resolve complaints with evidence."
Description

Capture and enforce explicit consent for messaging channels (e.g., SMS opt‑in/STOP), maintain unsubscribe states, and apply jurisdictional rules and rate limits. Log every nudge decision and outcome with timestamp, recipient, channel, template version, content fingerprint, and reason for silencing or escalation. Provide tamper‑evident audit logs, export, and retention policies to support HOA transparency and dispute resolution.

Acceptance Criteria
SMS Consent Capture and STOP/START Enforcement
Given a resident has not opted in to SMS and an SMS nudge is queued, When the decision engine evaluates channels, Then the SMS channel is excluded, no SMS is sent, and reason code CONSENT_MISSING is logged. Given a resident texts START or YES to the Duesly shortcode, When the inbound message is received, Then SMS consent is recorded with timestamp and source=inbound and a confirmation SMS is sent within 60 seconds. Given a resident texts STOP to the Duesly shortcode, When the inbound message is received, Then unsubscribe state is stored within 10 seconds, a single confirmation SMS is sent, and all future SMS nudges are blocked until the resident texts START. Given a resident is unsubscribed from SMS, When any user attempts to manually send an SMS nudge, Then the send is blocked, an error is displayed, and the attempt is logged with reason UNSUBSCRIBED. Given a jurisdiction requires double opt‑in for SMS, When a resident provides web consent, Then a verification SMS is sent and only upon reply YES is consent activated for SMS nudges.
Household-Level Unsubscribe and Channel Preference Respect
Given a household has two co‑payers A and B where A is unsubscribed from SMS and B is opted in, When an SMS nudge is escalated, Then only B receives the SMS and A is silenced with reason UNSUBSCRIBED logged. Given co‑payer A pays the invoice in full, When the next nudge cycle runs, Then all pending nudges for that invoice to co‑payer B across all channels are canceled and reason PAID_BY_COPAYER is logged. Given co‑payer A prefers email and co‑payer B prefers push, When a nudge is sent, Then the initial nudge is delivered only via each recipient’s preferred opted‑in channel. Given a co‑payer updates their channel preferences or consent, When the change is saved, Then it takes effect before the next scheduled nudge and the preference change event is added to the audit log.
Jurisdictional Quiet Hours and Rate Limits Enforcement
Given quiet hours are configured as 21:00–08:00 local time for the recipient, When a nudge is scheduled within that window, Then the send is deferred until 08:00 and decision reason QUIET_HOURS is logged with the calculated next send time. Given rate limits are configured as max 1 nudge per channel per account per 24 hours and max 3 nudges across all channels per 7 days, When a new nudge would exceed a limit, Then the send is blocked and decision reason RATE_LIMITED is logged including counters before and after. Given a recipient is on a do‑not‑contact suppression list for a channel, When a nudge is evaluated, Then that channel is blocked for the recipient and decision reason DO_NOT_CONTACT is logged. Given a recipient has no explicit timezone, When evaluating quiet hours, Then the community timezone is applied and the chosen timezone is recorded in the audit event.
Comprehensive Nudge Decision Logging
Given any nudge is evaluated, Then an audit event is written capturing: event_id, ISO‑8601 UTC timestamp, actor, community_id, household_id, payer_id, invoice_id, channel, provider, template_id, template_version, content_fingerprint (SHA‑256 of rendered content), decision (send|silence|escalate|defer), decision_reasons[], jurisdiction_ruleset_version, rate_limit_snapshot, consent_state_snapshot, and correlation_id. Given a nudge is sent successfully, Then outcome fields include provider_message_id, delivery_status, and delivery_timestamp; on failure, error_code and error_description are recorded. Given a nudge is silenced or deferred, Then no provider_message_id is present and at least one decision reason is recorded. Given a template or its variables change, When the next nudge is evaluated, Then template_version and a new content_fingerprint are recorded in the audit event.
Tamper‑Evident Audit Log Integrity
Given a contiguous sequence of audit events, When integrity verification runs, Then each event’s event_hash (SHA‑256 over canonical payload + prev_event_hash) and system signature validate for 100% of events. Given any stored event is altered, When integrity verification runs, Then the job fails and identifies the first invalid event by event_id and timestamp and no further writes are allowed until an admin acknowledges. Given append‑only storage is enforced, When an API client attempts to update or delete an audit event, Then the request is rejected with HTTP 403 and the attempt is recorded in a security audit log. Given a disaster recovery restore completes, When integrity verification runs post‑restore, Then the audit log passes verification before message sending resumes.
Audit Log Export and Retention Policy Compliance
Given an authorized board admin with Audit Export permission selects a community and date range, When an export is requested, Then a downloadable CSV and JSON are produced within 2 minutes containing all required audit fields plus a file‑level checksum and signature. Given role‑based redaction rules apply, When an export is generated, Then sensitive fields (e.g., phone and email) are masked according to role and jurisdiction, and the export action is logged with requester_id and IP. Given a retention policy of 24 months is configured (min 12, max 84), When a purge job runs, Then only events older than 24 months are purged or archived, a purge manifest with counts is created, and newer events remain available. Given an export requests data older than the retention window, When archives are disabled, Then the API returns 404 NOT_FOUND_FOR_RANGE; when archives are enabled, Then the export is queued and completes within 24 hours.
Dispute Resolution Timeline and Evidence View
Given a board member opens the Nudge Timeline for an invoice, When the page loads, Then all nudge decisions for that invoice and its co‑payers are shown in chronological order with channel, decision, reason codes, and delivery outcomes and each entry links to its audit event via correlation_id. Given a consent dispute on a recipient, When viewing the recipient profile, Then current and historical consent states are displayed with source (web, inbound SMS, admin), timestamps, and consent text snapshots where applicable. Given Generate Evidence PDF is requested for an invoice, When the job completes, Then the PDF bundle includes the timeline, consent evidence, configuration snapshot (rate limits, quiet hours), and an integrity summary with latest verified event hash and verification timestamp. Given the UI detects a missing or invalid audit event for a timeline entry, When exporting evidence, Then the export is blocked and the UI displays an integrity warning with steps to re‑verify logs.
Nudge Analytics and Optimization
"As a community manager, I want to see which nudges drive faster full payment so that I can tune policies and reduce reminder volume."
Description

Deliver dashboards and reports that track open, click, reply, and pay‑through rates, time‑to‑payment, and a noise score per unit and per community. Support cohort analysis by channel, template, and escalation path, plus A/B testing to optimize cadence and messaging. Feed key learnings back into the orchestrator via tunable weights to continuously improve who gets nudged and when.

Acceptance Criteria
Community and Unit Nudge Performance Dashboard
Given a board user with access to a community, when they open Nudge Analytics > Performance Dashboard and select a date range, then for the community and each unit row the dashboard displays Open Rate, Click Rate, Reply Rate, Pay‑Through Rate, Median Time‑to‑Payment, and Noise Score, each with a numerator/denominator definition tooltip. Given the user applies filters by channel, template, and escalation stage, when the filters are changed, then all metrics and counts recompute and render within 2 seconds for datasets up to 10k units and 1M events, and within 5 seconds for up to 100k units and 10M events. Given new events arrive, when the dashboard shows Last Updated, then data freshness is ≤15 minutes behind real time. Given the user clicks Export, when a CSV is generated, then metric totals in the file match the on‑screen values within a rounding tolerance of 0.1 percentage point and include unit/community IDs. Given no data matches a filter, when the dashboard renders, then an explicit No data state is shown and no stale metrics are displayed.
Cohort Analysis by Channel, Template, and Escalation Path
Given a date range, when the user selects Cohort Analysis and groups by Channel, Template, and Escalation Path, then the system displays for each cohort: cohort size (unique payer recipients), Open/Click/Reply/Pay‑Through rates, Median Time‑to‑Payment, and average Noise Score. When two cohorts are selected for comparison, then absolute and relative differences are shown with 95% confidence intervals; statistical significance is indicated when p<0.05; cohorts with n<100 are labeled Insufficient sample and excluded from significance flags. Given combined filters (e.g., SMS + Template T12 + Escalation L2), when applied, then cohort metrics recompute within 5 seconds and the applied filters are shown as chips and can be cleared individually. Given a configured view, when the user saves it, then it can be reloaded via a shareable URL and preserves filters, groupings, and date range.
A/B Test Setup and Results for Nudge Cadence and Messaging
Given an authorized user, when they create an A/B test specifying scope (community/segment), randomization unit (unit or payer), variants (templates/cadence), primary metric (e.g., pay‑through within 14 days), minimum sample or end date, and significance level (alpha≤0.05), then the platform validates inputs and assigns eligible targets 50/50 without splitting co‑payers across variants. When the test is running, then all related nudge events and outcomes carry an Experiment ID, and the Results page shows per‑variant: cohort size, Open/Click/Reply/Pay‑Through rates, Median Time‑to‑Payment, confidence intervals, and current p‑value for the primary metric. Given stop/declare rules are met (reached minimum sample and p<alpha), when Auto‑declare winner is enabled, then the winner is set and the orchestrator applies the winning variant to new nudges in scope within 30 minutes; the losing variant is stopped. When the user exports results, then a CSV with variant metrics and assignment logs is generated and downloadable within 60 seconds.
Orchestrator Weight Tuning and Continuous Optimization
Given tunable weights exist for orchestration (channel preference weight, responsiveness score weight, co‑payer role weight, noise penalty weight, time‑of‑day weight), when an authorized user updates weights for a community, then the change is versioned with effective timestamp, author, description, and is audit‑logged. When weights are changed and saved, then the next orchestration run (scheduled or manual) uses the new weights within 15 minutes, and assignment logs include the weight version used. Given a rollback action, when a prior version is restored, then subsequent orchestration runs reflect the restored weights and the audit log records the rollback. Given a simulation request with a sample cohort, when run, then the simulator returns projected changes in who/when gets nudged and projected noise score impact within 60 seconds, without sending any real nudges.
Noise Score Calculation and Suppression Rules
Given the default noise score formula (sum over last 14 days of channel_weight × nudges with 0.85 daily decay; email=1, SMS=2, push=0.5), when computed, then each unit and the community aggregate display a noise score rounded to one decimal and updated at least hourly. When a unit’s noise score exceeds the community threshold (default 10, configurable 5–20), then the orchestrator suppresses non‑critical nudges to that unit until the score drops below threshold or a payment posts; the suppression reason appears in the unit’s nudge log. Given the dashboard Noise tab, when opened, then a histogram of noise scores and the top 20 noisiest units are shown with filters and exportable CSV.
Data Quality, Attribution, and Privacy Controls
Given event collection, when a nudge is sent, opened, clicked, replied to, escalated, or leads to payment, then a single idempotent event with unique Event ID, timestamp, payer and unit IDs, community ID, channel, template ID, escalation stage, and (if applicable) Experiment ID is recorded; duplicates do not inflate metrics by more than 0.5%. When a payment is posted within the pay‑through window, then it is attributed to the most recent nudge prior to payment unless an active experiment specifies a different attribution model; attribution rules are documented and visible via tooltip. Given role‑based access, when a board member views analytics, then they only see communities they administer; exports redact PII (names, emails, phone numbers) unless the user has Finance Admin role. Given data SLAs, when analytics are queried, then event ingestion latency is ≤15 minutes at the 95th percentile and weekly data completeness ≥99.5%, with a status banner if SLAs are violated.
Events and Integrations for Nudge Sync
"As a platform integrator, I want reliable events and webhooks for nudge and payment updates so that our back‑office systems stay in sync without polling."
Description

Publish webhook events for nudge lifecycle (scheduled, sent, delivered, failed, silenced), invoice state changes, and payments received; consume payment processor webhooks to update state in near real time. Provide idempotency keys, retries with backoff, and signing for security. Integrate with Duesly’s feed and billing so that posts reflect the latest state and no duplicate reminders are generated by other automations.

Acceptance Criteria
Publish Nudge Lifecycle Webhooks
Given a nudge is scheduled for an invoice with multiple co-payers When the schedule is created or updated Then publish a nudge.scheduled webhook with event_id, idempotency_key, nudge_id, invoice_id, payer_ids, occurred_at, and sequence within 5 seconds (p95) Given a nudge is sent via any channel When the send attempt is initiated Then publish a nudge.sent webhook with channel, attempt_id, occurred_at, and sequence Given a delivery receipt or read confirmation is received When the provider confirms delivery Then publish a nudge.delivered webhook including provider_message_id and occurred_at Given a send attempt fails due to provider error or bounce When the attempt is marked failed Then publish a nudge.failed webhook with error_code and error_message Given a co-payer completes payment for the invoice When the invoice balance reaches zero Then publish a nudge.silenced webhook for remaining co-payers and stop any pending nudges for them Given any webhook is published When the HTTP request is made Then include Idempotency-Key, Duesly-Signature, and Duesly-Timestamp headers
Consume Payment Processor Webhooks in Near Real Time
Given the payment processor sends a webhook with a valid signature When the event is received Then verify signature and timestamp and return 2xx only after durable persistence Given a valid payment.succeeded for invoice X When processed Then mark invoice X as Paid within 10 seconds (p95), record payment_id and amount, and update the Duesly feed post to Paid Given a valid payment.failed or dispute.created for invoice X When processed Then update invoice state accordingly and append an entry to the Duesly feed with reason Given a duplicate processor event_id is received When processing the event Then do not create duplicate payments, state transitions, feed entries, or nudges (idempotent) Given invoice X becomes Paid from any processor event When state is updated Then cancel or mark as silenced all pending/scheduled nudges for all co-payers and emit corresponding nudge.silenced webhooks
Idempotency and Deduplication Across Events
Given an outbound webhook delivery is retried When the retry is sent Then reuse the same event_id and Idempotency-Key so subscribers can deduplicate Given two inbound processor webhooks with the same processor event_id arrive in any order When processing them Then exactly one state change is applied and no duplicate nudges or feed updates occur Given the service restarts during processing When it resumes Then previously processed inbound processor event_ids are still recognized for at least 30 days and not re-applied Given concurrent updates hit the same invoice (e.g., payment and nudge scheduling) When events are generated Then each event carries a stable sequence and occurred_at so downstream systems can deduplicate by (aggregate_id, sequence)
Retry Policy with Exponential Backoff and Dead Lettering
Given an outbound webhook request times out after 3 seconds or receives a non-2xx response When delivery fails Then schedule retries with exponential backoff and jitter starting at 30 seconds, doubling up to a max delay of 6 hours, for a maximum of 10 attempts Given a subscriber returns HTTP 429 with a Retry-After header When scheduling the next attempt Then honor Retry-After up to the 6-hour maximum Given all retry attempts are exhausted without a 2xx response When the delivery is abandoned Then move the event to a dead-letter queue with failure metadata and surface an alert/metric Given a previously failing endpoint recovers When a new event is published Then retries do not block publishing of new events and ordering per aggregate is preserved by sequence numbers
Webhook Signing, Timestamps, and Replay Protection
Given an outbound webhook is sent When headers are added Then compute Duesly-Signature as HMAC-SHA256 over timestamp and body using a shared secret and include Duesly-Timestamp in seconds Given a receiver validates the request When timestamp skew exceeds ±5 minutes or signature validation fails Then the receiver can reject with 4xx and Duesly will retry per policy Given an inbound processor webhook is received When signature verification per processor spec fails Then respond 400 and do not mutate any state Given an outbound webhook with a previously seen (timestamp, signature, Idempotency-Key) is replayed When the receiver detects the replay Then it can safely ignore due to stable event_id and Idempotency-Key
Causal Ordering and Event Metadata Integrity
Given events are emitted for the same invoice or nudge aggregate When multiple state transitions occur Then publish events with monotonically increasing sequence values and accurate occurred_at times based on the business action, not delivery time Given a test flow where a nudge is scheduled then sent Then no nudge.sent event is published before nudge.scheduled for the same aggregate (by sequence) Given retries or network reordering occur When subscribers receive events Then they can reconstruct order using (aggregate_id, sequence) without gaps or duplicates Given an event payload is constructed When validation runs Then required fields (event_id, aggregate_id, type, sequence, occurred_at, idempotency_key) are present and conform to schema
Prevent Duplicate Nudges and Synchronize Feed/Billing
Given a payment is recorded for any co-payer on an invoice When the remaining balance is zero Then cancel all pending/scheduled nudges for all co-payers, emit nudge.silenced, and ensure no further automated nudges are generated by other automations for that invoice Given a partial payment is recorded and balance remains > 0 When updating schedules Then adjust future nudge amounts to remaining balance, keep co-payer preferences, and avoid creating duplicate schedules Given a nudge.silenced event exists for a payer and invoice When a new automation run evaluates reminders Then do not generate a new nudge unless a new balance is created after a new charge Given any state change occurs (payment, failure, silence) When reflecting in UI Then the Duesly feed post for the invoice updates within 10 seconds (p95) with the latest status and audit trail

Role Presets

Invite co‑payers with simple, safe permission templates—Tenant, Roommate, Co‑Owner, Payer‑Only. Control what they can see (balances, notices), what they can do (pay, set autopay, view receipts), and which methods they can use, with one‑tap revoke and an audit trail for every change.

Requirements

Role Preset Library & Community Overrides
"As a community manager, I want to assign predefined role presets so that I can quickly invite co‑payers with the right permissions and reduce setup errors."
Description

Provide a library of safe, preconfigured role presets (Tenant, Roommate, Co‑Owner, Payer‑Only) that map to a vetted permission set covering visibility (balances, notices, compliance items, announcements), actions (pay, set/cancel autopay, manage payment methods, view receipts, comment), and constraints. Allow community administrators to enable/disable presets and create community-level overrides or clones with guarded toggles and built-in guardrails to prevent over-permissioning. Presets are versioned so future changes apply to new assignments while existing assignees can be migrated with an explicit choice. Integrates with the invite flow, account profiles, and the feed to ensure consistent semantics across modules and multiple properties per user. Expected outcome: faster, error-resistant onboarding of co‑payers and consistent least-privilege access across the platform.

Acceptance Criteria
Default Preset Library Visibility and Metadata
Given a community with no custom overrides When an administrator opens the Role Preset Library Then the system lists exactly four default presets: "Tenant", "Roommate", "Co‑Owner", "Payer‑Only" And each preset displays a semantic version identifier (e.g., v1.0.0) And each preset shows a read‑only base permission matrix for visibility (balances, notices, compliance items, announcements) and actions (pay, set/cancel autopay, manage payment methods, view receipts, comment) And default presets are enabled and selectable for use in this community by default And viewing the library does not alter any existing user assignments
Enable/Disable Presets at Community Level
Given a preset is currently enabled for a community When an administrator disables the preset in Community Settings Then the preset is removed immediately from all invite and role assignment pickers for that community And existing assignees of that preset retain their current access until explicitly changed And an audit log entry is recorded with actor, timestamp, community, preset name, prior state, new state When the administrator re‑enables the preset Then it becomes available again in pickers without altering existing assignees And a corresponding audit log entry is recorded
Create Community Override with Guardrails
Given an administrator selects "Create Override/Clone" on a base preset When they attempt to enable a permission outside the preset’s allowed scope Then the system blocks the change, displays an explanation of the violated guardrail, and disables Save When the override remains within allowed scope and the admin provides a unique name Then Save creates a community‑scoped preset starting at version v1.0.0 And a diff view versus the base preset is presented prior to confirmation And an audit record is captured including the full permission delta and approver
Preset Versioning and Assignee Migration
Given a new global version of a base preset is published When a community administrator opens the Role Preset Library Then new assignments use the latest version by default And existing assignees remain pinned to their previous version until migrated explicitly When the administrator initiates migration Then a wizard shows permissions added/removed/changed and the count of affected assignees And the administrator can migrate all or a selected subset after explicit confirmation And post‑migration, each assignee reflects the new version on their profile and an audit entry is recorded per assignee and as a summary
Invite Flow Uses Enabled Presets and Enforces Permissions
Given a community with certain presets enabled and others disabled When an admin invites a co‑payer Then only enabled presets and community overrides appear in the role picker And selecting a preset shows a summary of allowed visibility and actions including allowed payment methods When the invitee accepts and signs in Then the feed shows only the permitted visibility scopes for the assigned preset And checkout hides or blocks disallowed payment methods and actions (e.g., autopay, manage methods, view receipts) with 403 on prohibited API calls And the assignee’s profile displays the assigned preset name and version and provides a one‑tap Revoke that removes access immediately and logs an audit entry
Multi‑Property Role Isolation and Consistent Feed Semantics
Given a user has roles in multiple properties with different presets and versions When the user switches between properties Then the feed, balances, notices, compliance items, announcements, and payment capabilities reflect only the active property and its assigned preset And attempting to access data or actions from a non‑active property is denied with 403 and no data leakage occurs And role or preset changes in Property A do not alter the user’s role or access in Property B And the user’s account profile shows per‑property preset and version badges for clarity And all cross‑property changes are captured in per‑property audit logs
Secure Role Invitations with Tokenized Onboarding
"As a homeowner, I want to invite my tenant with a secure link so that they can pay dues without seeing my owner-only information."
Description

Enable owners and managers to invite co‑payers via email or SMS using single-use, time-limited tokens tied to a specific unit/account. The onboarding flow verifies identity with lightweight checks (e.g., last name and street number or a manager-provided code), captures consent to role terms, and surfaces what the invitee will be able to see and do before acceptance. Unaccepted invites auto-expire with optional reminder nudges. On acceptance, the user account is created or linked, the preset is applied, and access is granted immediately. Integrates with notifications, account linking, and the feed so invited users land with context and next steps. Expected outcome: a simple, secure path to add co‑payers without sharing passwords or overexposing data.

Acceptance Criteria
Email/SMS Invite Generates Single‑Use, Time‑Limited Token
Given an owner or manager initiates a role invite for a specific unit/account and selects a preset and expiry window When the invite is sent via email or SMS Then the system generates a unique, single-use token bound to the unit/account, selected preset, recipient contact, and an expiry timestamp And the invite message contains a link embedding the token and no sensitive data beyond necessary routing info And the token cannot be redeemed more than once; subsequent attempts return a "used/invalid" state And the token cannot be redeemed after expiry; attempts return an "expired" state And the token cannot be used to access or alter any unit/account or preset other than the one it was issued for And invite creation is recorded in the audit trail with actor, unit/account, preset, recipient contact (hashed/masked), channel, and expiry
Lightweight Identity Verification Gate
Given an invitee opens the tokenized invite link When prompted for verification Then the system accepts either (a) last name + street number matching the invited unit/account, or (b) a manager-provided code if that option was configured for the invite And on three consecutive failed attempts, verification is locked for 15 minutes for that token and device/browser And error messages remain generic and do not disclose which field was incorrect And all verification attempts (success/failure, timestamp, IP/device fingerprint) are logged in the audit trail
Role Preview and Explicit Consent Before Acceptance
Given the invitee passes verification When the role preview screen is displayed Then the invitee is shown an explicit list of what they will be able to see (e.g., balances, notices) and do (e.g., pay, set autopay, view receipts), and which payment methods are allowed, derived from the selected preset And links to the role terms and privacy policy are visible and accessible And the Accept/Continue action is disabled until the invitee checks an explicit consent checkbox And upon acceptance, the system records consent timestamp, terms version, IP, and user agent in the audit trail
Auto‑Expiration and Reminder Nudge for Unaccepted Invites
Given an invite is created with an expiry timestamp and reminders enabled When the invite remains unaccepted approaching expiry Then one reminder is sent via the original channel 24 hours before expiry (or immediately if created with less than 24 hours remaining) And at expiry, the invite status changes to Expired and the link resolves to an "invite expired" screen offering a request-new-invite action that notifies the inviter And no further reminders are sent after expiry And reminder and expiry events are logged in the audit trail with timestamps and delivery outcomes
Account Linking or Creation on Acceptance
Given the invitee accepts the role terms When the system processes the acceptance Then if a Duesly account exists for the invitee’s email/phone, the invited role is linked to that existing account without creating a duplicate user And if no account exists, a new user account is created using the invitee’s verified contact, with minimal required profile fields And in both cases, the invite is marked Consumed, and the user is authenticated into the account And the linkage/creation event is logged in the audit trail with user ID, unit/account, role preset, and inviter
Immediate Access with Preset Applied and Contextual Landing
Given the invitee’s acceptance is processed When access is granted Then the selected role preset permissions and restrictions are applied immediately to the user for the invited unit/account And the user lands on the community feed with a contextual banner summarizing their permissions and next best actions (e.g., pay balance, set up autopay) consistent with the preset And any disallowed actions (e.g., viewing owner-only notices or restricted payment methods) are blocked with a consistent 403-style UI message and are not executed And an acceptance event is logged in the audit trail and any configured owner/manager notifications are sent
Permission Matrix Enforcement Across Modules
"As a board member, I want permissions to be enforced consistently across the app so that co‑payers only access what they are allowed to see and do."
Description

Implement a centralized permission policy engine that evaluates role presets and context to gate capabilities across API, UI, and data layers. Enforce visibility of balances, statements, notices, compliance cases, and announcements; restrict actions such as making payments, setting/canceling autopay, adding/removing payment methods, viewing receipts, exporting statements, and commenting. Provide deterministic precedence when a user holds multiple roles on a unit or across units and ensure server-side authorization backs all client affordances. Include comprehensive unit tests and integration tests, and telemetry to detect and alert on policy denials. Expected outcome: consistent, auditable enforcement of least‑privilege access regardless of entry point or client state.

Acceptance Criteria
Block Unauthorized Payment and Autopay Actions
Given a signed-in user whose effective policy for Unit U denies payments.create, autopay.write, and payment_methods.write When the user invokes POST /units/{U}/payments via API Then the response is 403 with body containing error_code=policy_denied, policy_id, unit_id=U, and correlation_id And no payment record, ledger entry, or webhook is created And a telemetry event policy.denied is emitted with action=payments.create and user_id Given the same user visits the Unit U bill in the UI When the page renders Then the Pay button, Add Payment Method, and Set Autopay controls are not visible And attempting to force-enable the controls via client console still results in a 403 on submit Given the same user invokes DELETE /units/{U}/autopay When effective policy denies autopay.write Then the response is 403 and no schedule changes occur within the scheduler store
Tenant Visibility Gate Across Feed, Statements, Notices, and Compliance
Given a user with role preset Tenant on Unit U and no other roles When they query GET /units/{U}/balances and GET /units/{U}/announcements Then responses are 200 and include only data scoped to Unit U Given the same user queries GET /units/{U}/statements/export, GET /units/{U}/compliance_cases, and GET /units/{U}/notices/internal Then each response is 403 policy_denied And the UI hides Export Statement, Compliance, and Internal Notices sections Given the same user attempts to view another unit V they do not belong to via GET /units/{V}/balances or by altering UI routes Then the response is 404 or 403 with no data leakage (response size < 1 KB, no PII fields present) Given the same user opens an announcement thread When they try to post a comment and the policy denies comments.create for Tenant Then the UI disables the composer and API returns 403 on POST /announcements/{id}/comments
Deterministic Least-Privilege Precedence for Multi-Role Users
Given a user holds Co-Owner and Payer-Only on Unit U and Roommate on Unit V When evaluating action statements.export on Unit U where Co-Owner allows and Payer-Only denies Then the decision algorithm deny-overrides returns Deny And the audit decision record includes evaluated_roles=[Co-Owner,Payer-Only], algorithm=deny_overrides, winning_rule_id Given the same user evaluates payments.create on Unit V where Roommate allows Then the decision returns Allow for Unit V only and Deny for any other unit Given role changes occur (removing Payer-Only on Unit U) When re-evaluating statements.export Then the decision returns Allow within <= 5 seconds of the change across API and UI endpoints And telemetry emits policy.decision_changed with before=deny after=allow and correlation_id
Immediate Revoke with Audit Trail and Cache Invalidation
Given an admin uses one-tap revoke to remove a user's role on Unit U at T0 When the user attempts any previously allowed action (e.g., payments.create, receipts.read, comments.create) after T0 Then all affected API calls return 403 within <= 5 seconds of T0 And UI affordances are hidden or disabled on next navigation or hard refresh Then an immutable audit log entry is written with fields {actor_id, subject_user_id, unit_id: U, removed_roles, timestamp≈T0, reason} And the decision cache for subject_user_id+U is invalidated within <= 3 seconds And active sessions receive a policy_version increment and must re-fetch permissions before next privileged action
End-to-End Authorization Coverage via Automated Tests
Given the CI pipeline runs on main When unit tests for the policy engine execute Then line and branch coverage for the policy package are each >= 90% And mutation tests yield a score >= 70% Given integration tests run against a seeded environment with all role presets When tests exercise each protected action (balances.read, statements.export, notices.read_internal, compliance.read, payments.create, autopay.write, payment_methods.write, receipts.read, comments.create) via API and UI Then each allowed/denied outcome matches the policy matrix And no test can bypass server-side authorization by modifying client state Given a policy definition changes When snapshot tests compare effective permissions per role preset Then the pipeline fails unless snapshots are reviewed and updated by an authorized reviewer
Policy Denials Telemetry and Alerting
Given any 403 policy_denied response from API or GraphQL When the response is generated Then a structured event is emitted with fields {user_id, unit_id, action, resource, role_context, decision_algorithm, policy_id, http_path, correlation_id, ts} And the event is visible in dashboards within <= 15 seconds Given denials for a single action+unit exceed 10 within 5 minutes When the alerting rule evaluates Then a P2 alert is created with runbook link and recent samples And deduplication prevents duplicate pages for the same fingerprint within 15 minutes Given telemetry is unavailable When a denial occurs Then the API still returns 403 And a fallback local log is written with the same fields for later shipping
Role-Based Payment Method Controls
"As a treasurer, I want to restrict certain roles to ACH only so that we minimize card fees and reduce payment risk."
Description

Allow communities to constrain which payment rails and terms are available by role, including ACH-only, card allowed/disallowed, processing fee display, transaction amount limits, and whether a role may store payment methods on file. Enable role-scoped autopay permissions with configurable caps and guardrails (e.g., max per cycle, stop after balance clears). Integrate with the payment processor for tokenization and with billing to surface only eligible options. Handle edge cases such as shared payment methods across units without leaking data. Expected outcome: lower processing costs and risk while preserving a streamlined payment experience for permitted roles.

Acceptance Criteria
ACH-Only Payments for Tenant Role
Given the community config sets Tenant role to ACH-only and disallows cards And a user with Tenant role for unit U opens the Make a Payment flow When the payment method options are requested (UI and API) Then only ACH options are returned and rendered And card options are not rendered and are omitted from API responses And attempts to submit a payment with method_type=card return HTTP 403 with error_code=ROLE_RAIL_NOT_ALLOWED And any previously saved card tokens remain unusable for this role (UI-hidden, API 403) while remaining available to roles permitted to use them And an audit log entry is recorded for any blocked attempt with actor, role, method_type, timestamp, and unit_id
Processing Fee Display for Card Payments by Role
Given the community config allows cards for Co-Owner with a 2.9% + $0.30 processing fee and requires fee disclosure And disallows cards for Tenant When a Co-Owner initiates a $100.00 card payment Then the UI shows a processing fee line item of $3.20 and a total of $103.20 with the required disclosure text And the receipt and export include a separate fee line and the total reflects fee+principal When the same user is switched to Tenant role and opens the payment flow Then card options are hidden and no processing fee information is shown And API responses for pricing include fee fields only when the selected role and method are eligible
Transaction Amount Limits Enforcement by Role
Given the Payer-Only role is configured with per-transaction min=$5.00, max=$1,000.00, and daily cap=$2,000.00 (applied to total charge including fees) And a user has Payer-Only role When the user attempts a $1,500.00 payment Then the UI blocks submission with a message indicating the $1,000.00 per-transaction max and the API returns HTTP 422 error_code=ROLE_LIMIT_EXCEEDED details.max_per_tx=1000.00 When the user completes a $900.00 payment and then attempts another $1,200.00 payment on the same calendar day Then the second attempt is blocked with message indicating remaining daily limit=$1,100.00 and the API returns HTTP 422 error_code=ROLE_DAILY_CAP_EXCEEDED remaining_daily=1100.00 When the user attempts a $3.00 payment Then the attempt is blocked for violating the min amount with HTTP 422 error_code=ROLE_MIN_TX_NOT_MET
Saved Payment Method Permission by Role
Given Roommate role: may not save payment methods; Co-Owner role: may save ACH only And a user with Roommate role adds a new ACH or card during checkout When the add-method form is presented Then the "Save for future use" option is not displayed And no tokenization request is sent to the processor And completing the payment does not create a reusable token When the same user is assigned Co-Owner role and adds an ACH account Then a token is created and stored with metadata role_scope=Co-Owner and unit_id And attempting to save a card as Co-Owner is blocked with UI message and API HTTP 403 error_code=ROLE_SAVE_METHOD_NOT_ALLOWED method_type=card And tokens become hidden/inaccessible if the user’s role later changes to one that disallows saved methods (API returns 403)
Autopay Setup and Guardrails by Role
Given Co-Owner role is permitted to enable autopay with max_per_cycle=$500.00 and rule stop_after_balance_clears=true And a Co-Owner opens Autopay settings for monthly dues When the user sets autopay amount to $450.00 Then the setup succeeds and the schedule shows max_per_cycle=$500.00 and stop_after_balance_clears=true When the user attempts to set $600.00 Then the UI prevents save and API returns HTTP 422 error_code=ROLE_AUTOPAY_CAP_EXCEEDED cap=500.00 When a statement of $700.00 posts and autopay runs Then only $500.00 is paid, the remaining $200.00 stays outstanding, and an event is logged with applied_amount=500.00 When the balance reaches $0.00 Then the next cycle’s autopay is automatically paused per stop_after_balance_clears and the user receives a confirmation notification
Shared Payment Method Privacy Across Units
Given a card token T is associated with Unit A (Co-Owner) and also authorized for Unit B (Tenant) via permitted sharing rules When a Tenant in Unit B views payment methods Then they see only masked metadata for T (brand, last4) scoped to Unit B and cannot see transactions, receipts, or balances from Unit A And listing transactions for T returns only Unit B charges for the Tenant; attempts to access Unit A charges return HTTP 403 error_code=FORBIDDEN_RESOURCE_SCOPE And UI and API do not expose owner PII beyond what is permitted by the Tenant’s role And audit logs record any cross-unit access attempt with outcome=blocked
One-Tap Revoke Updates Payment Capabilities Immediately
Given an Owner revokes a Roommate’s access via one-tap revoke When the revoke action is confirmed Then the user’s session tokens are invalidated within 60 seconds and new payments are blocked with HTTP 401/403 And all scheduled autopays for that role are canceled before the next run and the user is notified And saved methods remain at the processor but are detached from the revoked user’s role scope; methods still in use by other roles remain usable And the payment options UI for the revoked user shows no actionable methods if they attempt to return via stale link And an audit trail entry captures actor, target, changes (roles_before/after, permissions diff), timestamp, and IP
One‑Tap Revoke with Session Kill Switch
"As a manager, I want to revoke a role in one tap so that access is removed immediately when a tenancy changes."
Description

Provide immediate revocation of a role assignment with a single action available to owners and managers, including optional temporary suspension. On revoke, invalidate active sessions and tokens, cancel or pause pending autopay instructions for the affected role, and remove access to restricted content in the feed and billing immediately. Notify both the revoker and the affected user with a clear record of what changed. Provide bulk revoke for unit turnovers. Expected outcome: rapid, reliable removal of access during tenancy changes or disputes without support intervention.

Acceptance Criteria
One‑Tap Revoke by Owner/Manager for a Single Role
Given an owner or manager with permission is viewing a user’s role assignment for a unit When they tap the single Revoke control and confirm Then the role state transitions to Revoked within 1 second of server acknowledgement And the API returns 200 with roleId, previousState=Active, newState=Revoked, revokerId, timestamp And the UI updates the role to Revoked without requiring a page refresh And the operation is idempotent for 10 minutes; repeated requests with the same roleId return 200 and no additional side effects And an audit record is created with fields: actorId, targetUserId, unitId, rolePreset, action=Revoke, reason(optional), timestamp(ISO‑8601), ip, previousState, newState
Session and Token Kill Switch on Revoke
Given the affected user has active sessions (web, iOS, Android) and valid access/refresh tokens When the revoke action is completed (T0) Then all access and refresh tokens for the role’s tenant are invalidated within 5 seconds of T0 And any active WebSocket connections are closed within 5 seconds of T0 And subsequent API calls using old tokens receive 401/403 within 1 second of request And refresh attempts use blacklisted tokens and are rejected And server logs capture token invalidation count and device types for the event
Immediate Removal of Restricted Feed and Billing Content
Given the role is revoked or suspended When the affected user attempts to access the feed, billing pages, or deep links after T0 Then restricted posts, invoices, statements, and notices tied to the role are not returned by APIs (0 items) And deep links to previously accessible resources return 403 with error_code=ACCESS_REVOKED And cached client data is invalidated on next sync, removing restricted items within 5 seconds And push updates for restricted content are no longer delivered to the affected user
Autopay Cancellation/Pause on Revoke or Suspension
Given the role has active autopay instructions When Revoke is confirmed at T0 Then all future-dated autopay executions scheduled after T0 are canceled and will not execute And any autopay currently executing at T0 is allowed to complete but is marked with relation to the revoke event And the user’s autopay status is updated to Canceled and visible to revoker in billing settings When Suspend‑Until is confirmed at T0 with end time Te Then autopay executions scheduled in (T0, Te] are paused and do not execute And autopay status displays Paused‑Until Te and resumes automatically at Te
Notifications to Revoker and Affected User with Audit Trail
Given a revoke or suspend action completes When T0 occurs Then the revoker receives in‑app confirmation immediately and an email within 60 seconds containing: target user, unit, role, action, timestamp, autopay effects And the affected user receives a push (if enabled) and email within 60 seconds stating access change, unit, role, what changed, and next steps And an immutable audit entry is persisted with correlationId linking action, token kill, and autopay changes And notification delivery outcomes (sent, bounced) are logged against the audit entry
Bulk Revoke for Unit Turnover
Given a manager selects N roles across one or more units and initiates Bulk Revoke with a single confirmation When the batch is submitted with a client‑supplied idempotency key Then at least 95% of roles revoke successfully within 30 seconds for N≤100, and the remainder complete or are retried within 5 minutes And each role generates its own audit entry plus a batch summary audit with counts: total, succeeded, failed, retried And partial failures are reported with per‑role error codes and are safe to retry using the same idempotency key without duplicating side effects And all successful revocations trigger session kill and notifications as per single revoke
Temporary Suspension with Auto‑Reinstatement
Given an owner or manager selects Suspend‑Until with a specific end date/time Te for a role When the suspension is confirmed at T0 Then access is blocked immediately as with revoke, but the role state is Suspended‑Until Te And tokens are invalidated at T0 and not reissued until the user signs in after Te And autopay is paused (not canceled) during (T0, Te] And at Te the role automatically returns to Active, audit logs a Resume event, and no previously restricted historical data created during suspension becomes visible retroactively beyond the role’s permissions And the revoker and affected user are notified at T0 and at Te
Role Change Audit Trail
"As a compliance officer, I want a complete audit trail of role changes so that we can resolve access disputes and meet record‑keeping requirements."
Description

Record an immutable, filterable audit timeline for all role-related events, including invitation sent/accepted/expired, role assigned/updated/revoked, permission changes, and autopay capability toggles. Capture actor, target, timestamp, IP/device, reason, and before/after permission diffs, with export and retention aligned to compliance needs. Surface the audit within the unit’s activity history for managers and board members, and expose select entries to owners for transparency. Provide webhook/event stream hooks for downstream systems. Expected outcome: traceable governance of access decisions that reduces disputes and supports compliance obligations.

Acceptance Criteria
Immutable Event Recording for Role Lifecycle
- Given a role-related action occurs (invitation.sent, invitation.accepted, invitation.expired, role.assigned, role.updated, role.revoked, permissions.changed, autopay.toggled), when it is successfully committed by the backend, then exactly one audit event is appended within 1 second with a globally unique event_id and a monotonically increasing sequence per unit. - Given an existing audit event, when any user or API attempts to modify or delete it, then the request is rejected (HTTP 403/405) and no changes occur; only new correction events may be appended. - Given the audit API, when a client fetches an event multiple times, then the payload remains byte-for-byte identical across requests (immutability verification).
Comprehensive Event Metadata Capture
- Given an audit event is created, then it must include the following required fields: event_id, event_type, unit_id, timestamp (UTC ISO8601), actor_id (nullable for system), actor_type, target_user_id (or target_party_id), ip_address (IPv4/IPv6), user_agent, and reason_code (enum or "unspecified"). - Given a permissions.changed or role.updated event, then before_permissions and after_permissions are captured as serialized lists; given an autopay.toggled event, then autopay_enabled_before/after and payment_method_whitelist_before/after are captured. - Given a client attempts to supply a timestamp, then the server ignores client time and sets server time; if any required field is missing/invalid, event creation fails with HTTP 400 and a validation error code. - Given any single event payload, then its stored size does not exceed 10 KB; otherwise the write is rejected with HTTP 413.
Audit Timeline Filtering and Search
- Given a manager views a unit’s audit tab, when applying filters (date range up to 5 years, event_type multi-select, actor_id, target_user_id, reason text contains), then results return in ≤ 2 seconds for up to 50,000 matching events, sorted newest-first, with stable pagination (50 per page). - Given no filters are applied, then the default view shows the last 90 days of events for the unit. - Given pagination, when navigating pages or changing sort, then the total count and page results remain consistent with the applied filters (no duplicates or gaps).
Export and Retention Policy Compliance
- Given filters are applied in the audit tab, when Export CSV or Export JSON is requested, then a downloadable file is produced within 60 seconds for up to 100,000 events, containing all event fields plus event_id, and the export is logged as export.created with actor and filter summary. - Given an export is generated, then the download URL is pre-signed and valid for 24 hours; after expiry, access returns HTTP 403. - Given the community retention period is configured between 1 and 10 years (default 7), when the nightly retention job runs, then events older than the configured period are purged from UI, API, exports, and search, and an audit.purge summary event is recorded with counts. - Given a user attempts to change the retention setting, then only admins can do so; a confirmation step is required; and the change is audited with before/after values and actor.
Activity History Surface and Owner Visibility Scoping
- Given a manager or board member views a unit’s activity history, then the audit timeline is visible with full details for that unit, and selecting an entry reveals all metadata including IP/device and permission diffs. - Given an owner views their unit’s activity history, then only events of types [invitation.sent, invitation.accepted, invitation.expired, role.assigned, role.revoked, autopay.toggled] are shown; fields ip_address and user_agent are suppressed; other event types are not returned. - Given an owner attempts to access restricted audit events via the API, then the server returns HTTP 403 and no event data is leaked.
Webhook/Event Stream Delivery and Retry
- Given a subscriber configures a webhook with a secret and selected event types, when matching audit events are created, then Duesly delivers a signed (HMAC-SHA256) JSON payload including event_id, event_type, unit_id, actor, target, timestamp, and diffs within 5 seconds of event creation. - Given webhook delivery, then at-least-once semantics are provided with idempotency via event_id; retries use exponential backoff for up to 24 hours; HTTP 2xx stops retries; HTTP 5xx and network errors continue retries; HTTP 4xx (except 429) stop after 10 attempts to dead-letter; HTTP 429 respects Retry-After. - Given multiple events for the same unit_id, then webhook deliveries preserve ordering per unit_id. - Given a subscriber falls behind, then they can resume via an event stream API using last_event_id checkpoints and the same event schema.
One‑Tap Revoke and Autopay Toggle Auditing
- Given a manager uses one‑tap revoke on a role, when the action is confirmed, then a role.revoked event is appended within 1 second capturing actor, target, reason_code, before/after permissions, timestamp, ip_address, and user_agent, and it appears in the unit’s audit timeline. - Given a user toggles autopay capability for a role, then an autopay.toggled event is appended capturing before/after autopay_enabled and any payment method whitelist changes, and it is visible/delivered according to role-based visibility and webhook configurations. - Given exports or filters are used, then both role.revoked and autopay.toggled events are included and searchable by event_type.

Split Rebalance

When fees, credits, or partials land mid‑cycle, Duesly automatically recalculates each person’s share based on your split rules and payment timing. It resolves over/underpayments by applying proportional credits or optional refunds to the correct payer—keeping the unit ledger unified and disputes low.

Requirements

Real-time Split Recalculation Engine
"As a treasurer, I want mid-cycle rebalances to recalculate shares automatically based on timing and rules so that ledgers stay accurate without manual spreadsheets."
Description

Automatically recalculates each payer’s share whenever mid-cycle fees, credits, partial payments, reversals, or ownership changes occur, applying time-weighted proration and configured rounding rules. Posts resulting adjustments to the unit ledger and payer sub-ledgers atomically to keep balances synchronized with the Duesly feed. Ensures deterministic outcomes, supports backdated events, and prevents double-application by anchoring calculations to event timestamps and unique references.

Acceptance Criteria
Mid-Cycle Fee: Time-Weighted Split and Atomic Posting
Given an active billing cycle with configured percentage-based split rules and three payers And a currency rounding policy set to 2 decimal places When a new fee with uniqueReference and eventTimestamp T is posted mid-cycle Then the engine recalculates each payer’s share using time-weighted proration from T to cycle end And applies the configured rounding policy consistently across all payers And posts a single atomic transaction group containing one unit-ledger adjustment and payer sub-ledger adjustments that sum exactly to the fee amount And the Duesly feed shows a single consolidated post referencing the transaction group And the recalculation and postings complete within 2 seconds for up to 50 payers
Partial Payment: Proportional Application and Balance Synchronization
Given a unit with multiple payers and outstanding balances And a partial payment P is received for payer X with uniqueReference and eventTimestamp T When the payment is processed Then P is applied only to payer X’s sub-ledger according to standard allocation rules And the unit ledger reflects the receipt such that unit balance equals the sum of payer sub-ledger balances And no other payer’s balance changes as a result of P And postings to the unit ledger, payer sub-ledger, and Duesly feed occur atomically in one transaction group And the operation completes within 2 seconds And reprocessing the same uniqueReference does not create duplicate postings
Backdated Credit: Retroactive Recalculation Anchored to Event Timestamp
Given a credit event with uniqueReference and eventTimestamp Tb that falls within a prior or current cycle When the credit is posted Then the engine recalculates splits effective from Tb forward, adjusting any affected cycles deterministically And posts a single atomic transaction group of adjustments so that current balances reflect the retroactive credit And the Duesly feed displays the backdated nature with the original eventTimestamp And replaying the same uniqueReference results in no additional adjustments (idempotent) And recalculation completes within 5 seconds for up to 24 months of affected history
Ownership Change Mid-Cycle: Time-Sliced Shares and Optional Refund Handling
Given payer A’s responsibility ends at time T and payer B’s responsibility begins at time T for the same unit And a refundMode setting is configured as either Credit or Refund When the ownership change is recorded with uniqueReferences for the change event(s) Then the engine time-slices the cycle at T and recalculates each payer’s share for their active periods using time-weighted proration And applies the configured rounding policy with totals conserved at the unit level And if payer A has overpaid, then according to refundMode the engine posts either a credit to payer A’s sub-ledger (Credit) or a refund payable entry (Refund) And all adjustments are posted atomically to unit and payer ledgers with a single transaction group visible in the Duesly feed And results are deterministic when rerun with the same inputs
Payment Reversal: Correct Negative Adjustments and Recalculation
Given a previously applied payment with uniqueReference R exists for payer X And a reversal event arrives referencing R with its own uniqueReference and eventTimestamp Tr When the reversal is processed Then the engine posts negative entries equal to the amounts previously applied by R to payer X’s sub-ledger and the unit ledger And recalculates any dependent balances deterministically without altering unrelated payers And prevents double reversal by ignoring repeat reversals of R And posts all adjustments atomically with a single transaction group and updates the Duesly feed And completes within 3 seconds
Deterministic Replay: Stable Ordering and Identical Outcomes
Given a set of fee, credit, payment, reversal, and ownership change events each with eventTimestamp and uniqueReference When the engine processes the set twice in the same order Then unit and payer balances, rounding outcomes, and transaction group contents are identical across runs And when two events share the same eventTimestamp, ordering is resolved deterministically by uniqueReference And serialized exports of the unit ledger are byte-for-byte identical across runs And no nondeterministic variability (e.g., differing residual cent distribution) is observed
Concurrency and Idempotency: Simultaneous Events Without Double-Application
Given two or more distinct events with uniqueReferences arrive concurrently for the same unit When the engine processes them Then each uniqueReference is applied exactly once And the final state reflects all events with no duplicates and no missed applications And readers never observe partial state (either all adjustments in a transaction group are visible or none are) And end-to-end processing latency remains under 3 seconds at p95 for up to 10 concurrent events And system logs include correlation IDs linking each event to its transaction group for auditability
Split Rule Versioning and Hierarchy
"As a manager, I want to version and prioritize split rules with effective dates so that historic charges reflect the correct policy at the time they were applied."
Description

Provides configurable split rules (equal share, percentage by unit attributes, fixed amounts, caps/floors/minimums) with effective dates and priority order. Preserves historical rule versions and resolves which rule set applies at calculation time, including unit-level exceptions and overrides. Validates configurations, enforces rounding policies, and documents precedence so that historic charges remain explainable and reproducible.

Acceptance Criteria
Apply Correct Rule Version by Effective Date
- Given a charge with timestamp T and multiple rule versions with non-overlapping effective ranges, When calculating shares, Then use the version whose effective range includes T. - Given T does not fall within any published version, When calculating, Then return error code "no_active_rule_version" and do not produce shares. - Given a backdated adjustment at timestamp T2, When recalculating, Then use the rule version active at T2, not the current date. - Given two versions overlap at any instant, When publishing, Then block publish with validation error "overlapping_versions"; if already published, Then abort calculation with error "ambiguous_rule_version". - Given a version is retired, When explaining historical charges that referenced it, Then resolve to the archived immutable version and display its parameters.
Resolve Rule Precedence Across Hierarchy
- Given multiple applicable rules at time T across scopes (community, subgroup, unit, charge-level), When selecting a rule, Then choose the one with the highest precedence by: charge-level > unit > subgroup > community. - Given two rules at the same scope are applicable at T, When selecting, Then choose the rule with the lower numeric priority value (1 beats 2); if still tied, Then choose the latest published_at; if still tied, Then choose the lexicographically smaller rule_id to ensure determinism. - Given the chosen rule, When calculation completes, Then emit a precedence trail including candidate rules, sort order, and the chosen rule_id, scope, and priority. - Given a higher-precedence rule is deactivated, When recalculating at T, Then fall back to the next candidate deterministically and record the fallback in the audit trail.
Unit-Level Override with Effective Period
- Given a unit-level override for Unit U with effective range [E_start, E_end], When publishing, Then validate that it does not overlap another override for U at the same scope and that E_start <= E_end. - Given an override for U is active at time T, When calculating shares, Then apply the override to U only; all other units use their applicable parent rule. - Given an override for U expires at E_end, When calculating for T > E_end, Then revert U to the next applicable parent rule active at T. - Given any create/update/delete of an override, When saved, Then create an immutable audit record with actor, timestamp, change diff, and reason code; historical calculations reference the specific override version_id used.
Configuration Validation and Conflict Prevention
- For percentage-based rules, When publishing, Then require sum of all percentages == 100.00% within tolerance ±0.0001%; otherwise block with error "percentage_sum_invalid". - For fixed-amount splits, When publishing, Then require the sum of unit amounts equals the target total (or a defined subtotal); if not and auto-balance is disabled, Then block with error "fixed_sum_mismatch". - For caps/floors/minimums, When publishing, Then validate min <= floor <= cap and that each is non-negative; otherwise block with error "cap_floor_min_conflict". - For effective ranges at a given scope/priority, When publishing, Then reject configurations with any overlapping date/time intervals with error "overlapping_effective_ranges". - For rounding policy, When publishing, Then require selection from allowed set {HALF_UP, HALF_EVEN, DOWN}; otherwise block with error "rounding_policy_invalid".
Deterministic Rounding and Remainder Allocation
- Given currency precision p and rounding mode M, When computing each unit share, Then round each pre-rounded share to p using M. - Given the set of rounded shares, When summing, Then the total equals the rounded grand total; allocate any remainder cents deterministically by descending fractional part, then by stable unit key (e.g., unit_id asc). - Given rounding allocation, When producing outputs, Then include per-unit fields: pre_round_value, rounded_value, remainder_delta, and allocation_rank. - Given identical inputs, When recalculating, Then produce identical rounded outputs and allocation ordering (idempotent).
Historical Reproducibility and Audit Trace
- Given a calculation at time T using rule version V, When an explanation is requested, Then return an audit payload containing rule_id, version_id, scope, priority, effective_range, parameters, rounding_policy, and precedence trail. - Given historical data, When re-running the calculation with archived versions pinned, Then produce identical outputs and a stable calculation_hash that matches the original. - Given an export request, When exporting the rule version and audit trace, Then provide machine-readable JSON including all parameters and a checksum; the checksum verifies on re-import. - Given a published rule version, When attempting to edit it, Then block with error "immutable_version" and require creating a new version instead.
Mid-cycle Event Ingestion and Idempotency
"As a billing admin, I want the system to detect and process mid-cycle fees, credits, partials, and reversals idempotently so that duplicate or missed rebalances don’t occur."
Description

Ingests billable events from payment rails, bank sync, manual adjustments, and integrations, detecting mid-cycle changes that require rebalance. Queues events for processing, guarantees idempotency via source event IDs and hashes, and gracefully handles reversals and chargebacks. Locks closed periods, replays calculations when late updates arrive, and exposes processing status for observability.

Acceptance Criteria
Duplicate External Event Idempotency
Given a previously processed payment event with source_event_id=X and hash=H exists in the idempotency store When an identical event (same source_event_id and hash) is received from any ingestion source Then no new ledger entries are created and no rebalance job is scheduled And the ingest endpoint returns 200 with outcome=deduplicated and idempotency_key=X And processing status for X updates to deduplicated without incrementing retry_count And unit ledger balances and per-payer shares remain unchanged
Mid-cycle Partial Payment Triggers Rebalance Queueing
Given an open billing period and active split rules for a unit And a partial payment event E arrives with event_time within the current period When E is validated and persisted with source_event_id/hash Then E is enqueued for rebalance within 5 minutes of receipt And exactly one rebalance job is created and linked via correlation_id to E And E is marked requires_rebalance=true in processing status And observability shows state transitions: received -> validated -> enqueued
Reversal and Chargeback Handling
Given a settled payment event P previously applied to a unit ledger And a reversal/chargeback event R referencing P is received When P belongs to an open period Then R is applied as a compensating adjustment and a rebalance replay is triggered And processing status updates P.status=reversed and R.status=applied When P belongs to a closed period Then no mutations occur in the closed period; an adjustment is posted to the next open period linked to P And all actions are audit-logged with immutable links between P and R
Late Bank Sync Update Replays Calculation
Given a bank-synced credit or fee event arrives after initial allocations were calculated for an open period When the event is ingested and validated Then the system replays the calculation for the affected unit ledger using current split rules and payment timing And prior allocations are superseded by replay results; previous calculation snapshot is archived And processing status records replay_performed=true with before_hash and after_hash and replay_timestamp And total charged/credited amounts equal the sum of source events (no double-charging)
Closed Period Lock Enforcement
Given a billing period is marked closed and write-locked When a payment, credit, or manual adjustment with event_time in the closed period is ingested Then the event is persisted but not applied to the closed ledger And processing status is set to deferred_to_next_open_period with target_period_id populated And no rebalance job is created for the closed period And an audit log entry records deferral with reason=closed_period
Processing Status API and UI Observability
Given an authenticated board member or system integrator queries processing status for source_event_id X via API or UI When X exists and is processed or in-flight Then the response includes: state, timestamps (received, validated, enqueued, processing_started, completed), retry_count, error_code/message (if any), outcome (applied|deduplicated|deferred|failed), correlation_id, and links to created jobs/adjustments And p95 latency for the status endpoint is <= 500ms under normal load And failures schedule retries with exponential backoff up to 5 attempts; after max, the event moves to a poison queue with status=failed_pending_manual_review
Proportional Credits and Refund Handling
"As a treasurer, I want over/underpayments to be resolved as proportional credits or optional refunds according to policy so that payers are treated fairly and balances net to zero."
Description

Automatically resolves over/underpayments by allocating proportional credits to future charges or issuing refunds per policy, with configurable thresholds, payer preferences, and refund methods. Applies adjustments to the correct payer, supports partial and full refunds, and prevents negative balance drift. Records all actions in the ledger and reconciles against settlement data to ensure accuracy.

Acceptance Criteria
Proportional Credit Allocation After Overpayment
Given a unit charge of $100 with split rules Payer A 60% and Payer B 40% and Payer A submits a $120 payment that has settled When Split Rebalance runs Then $60 is applied to Payer A’s share of the open charge And $40 remains due for Payer B (no cross-payer transfer) And $60 is recorded as a payer-level credit for Payer A for future charges And all ledger entries (payment application, credit creation) are posted with payer ID, unit ID, amount, timestamp, and source transaction ID And allocation totals equal the payment amount to the cent with rounding half-up And no payer balance becomes negative as a result
Auto-Apply Credits to Future Charges with Priority
Given a payer credit balance on a unit and a new charge posts for that unit When the system evaluates available credits Then credits auto-apply to that payer’s portion of the new charge using the configured priority (oldest charge first) And application occurs within 5 minutes of the charge posting And credits do not apply to other payers or other units And partial application is supported when credit < payer portion And any remaining credit persists for future charges with an audit entry
Refund Issuance Per Policy Thresholds and Methods
Given a payer credit balance created by overpayment and a refund policy with a minimum threshold of $10 and preferred method ACH to original funding source When the credit balance is >= $10 and the originating payment has settled per processor data Then the system initiates a refund for the lesser of (credit balance, policy max if configured) to the preferred method And records refund initiation with transaction ID, amount, method, payer, unit, and correlation to the original payment And updates status to Settled only when processor confirms success And if settlement is not confirmed within the SLA, the refund remains Pending and is retried per policy without duplicating the refund And if credit < $10, the amount remains as a credit (no refund) with a ledger note citing the threshold
Respect Payer Preferences for Credit vs Refund
Given payer-level preferences specify Credit vs Refund behavior and a prioritized list of refund methods (e.g., original card, ACH on file, check) When an overpayment creates a credit balance Then the system evaluates the payer’s preference first And if Refund is preferred and allowed by policy, it attempts the first available method in priority order And if the top method is unavailable, it falls back to the next method and records the fallback reason And if no refund method is available, the balance remains as a credit and the payer is notified And all outcomes are captured in the ledger with preference snapshot and method used
Partial and Full Refund Handling
Given a payer credit balance of $25 created from prior overpayment and admin initiates a partial refund of $15 via the selected method When the refund request is submitted Then the system validates available credit >= $15 and that the originating payment(s) have settled And initiates a $15 refund and leaves $10 as remaining credit And posts ledger entries for refund initiation and settlement with amounts and references And prevents issuing a second refund for the same request via idempotency keys And supports full refund by issuing $25 when requested, leaving $0 remaining credit
Negative Balance Drift Prevention and Rounding Controls
Given multiple cycles of allocations, reversals, and refunds across a unit When the system posts adjustments Then no payer-level credit balance becomes negative unless offsetting an open payable on the same unit in the same transaction And the unit ledger net equals sum(charges) − sum(applied payments) − sum(refunds) ± sum(explicit adjustments) at all times And monetary rounding uses half-up to 2 decimals and any residual < $0.01 per unit per charge is captured as a rounding adjustment entry And attempts that would create unbalanced or negative drift are rejected with a clear error logged
Ledger Audit Trail and Settlement Reconciliation
Given credits are created or applied and refunds are initiated or settled When the daily reconciliation runs Then every ledger entry contains type, payer ID, unit ID, amount, currency, timestamp, actor (system/admin), source transaction ID, and correlation IDs And processor settlement reports are matched to refund transactions 1:1 by transaction ID and amount And any mismatch in amount, currency, or status is flagged within the same business day with an exception entry and alert And reconciliation Pass is recorded when deltas equal $0; otherwise Fail is recorded with details
Transparent Audit Trail and Reconciliation Export
"As a board member, I want a transparent audit trail and exports showing calculations and changes so that I can explain charges and pass audits."
Description

Captures a full audit trail of each rebalance: inputs, rule version used, formulas, before/after balances, actor, and timestamps. Surfaces human-readable explanations inline on ledger entries and provides exportable CSV/PDF reports and API access for audits. Supports search and filters by unit, payer, date range, and event type to streamline investigations and board reporting.

Acceptance Criteria
Capture Complete Rebalance Audit Trail
Given a Split Rebalance is executed for a unit due to a mid‑cycle fee, credit, or partial payment When the rebalance completes Then an audit record is created containing: event_id (UUID), unit_id, affected payer_ids, event_type="rebalance", rule_version, inputs (fees/credits/partials), formulas applied, before_balances and after_balances per payer, actor (user_id or "system"), and timestamp (ISO 8601 with timezone) And the audit record is retrievable by event_id from UI, export, and API within 1 second of completion And retrieving the audit record later returns the same data as originally recorded
Inline Human-Readable Ledger Explanation
Given a ledger entry originated from a rebalance When the ledger entry is viewed Then the entry displays a human‑readable explanation including: reason for change, calculation summary with per‑payer amounts and percentages, rule_version, and a link to the full audit record And the explanation appears in list view (truncated with "Read more") and in detail view (full text) And the explanation renders within 2 seconds and meets a readability level at or below grade 8
CSV Export of Rebalance Audit with Filters
Given filters for unit(s), payer(s), date range, and event type(s) are selected When a CSV export is requested Then the CSV contains only matching records, sorted by timestamp descending, with header columns: event_id, unit_id, payer_ids, event_type, rule_version, inputs, formulas, before_balances, after_balances, actor, timestamp And the CSV is UTF‑8 encoded, uses RFC 4180 quoting, timestamps are ISO 8601 with timezone, and the filename follows "community-audit-YYYY-MM-DD_to_YYYY-MM-DD.csv" And the export completes for up to 50,000 events within 30 seconds or returns an asynchronous download/progress link
PDF Reconciliation Report for Board Review
Given a board user selects filters for unit(s), payer(s), date range, and event type(s) When a PDF report is requested Then the PDF includes a summary section (event count and total net adjustments by type) followed by per‑event details (human‑readable explanation, event_id, unit_id, payer_ids, rule_version, before/after balances, actor, timestamp) And the PDF has page numbers and a cover section showing community name and date range; includes a table of contents when there are 20+ events And generation succeeds for up to 2,000 events within 60 seconds or provides an asynchronous download link
Audit API Endpoint with Filtering and Pagination
Given a valid API key with Board scope When GET /v1/audit-events is called with any combination of filters (unit_ids, payer_ids, date_from, date_to, event_types) and pagination (cursor, limit ≤ 500) Then the API responds 200 with a JSON array of matching audit events and a next_cursor when more data is available And each event includes: event_id, unit_id, payer_ids, event_type, rule_version, inputs, formulas, before_balances, after_balances, actor, timestamp And invalid parameters return 400, invalid/expired key returns 401, insufficient role returns 403; p95 response time ≤ 800 ms for up to 500 items
Search and Filter Behavior in UI
Given a board user opens the Audit Trail screen When they apply filters for unit, payer, date range, and event type and optionally enter a text search Then results update within 1 second, apply AND logic across filter types, support multi‑select for unit/payer/event type, and treat the date range as inclusive And an empty state shows a clear message with a "Clear filters" action; exporting from this view respects the current filters and sort
Access Control and Data Visibility
Given users with roles Board, Manager, and Payer When accessing audit trail entries, exports, and API Then Board and Manager roles can view all audit records and generate exports; Payers can only view human‑readable explanations for their own unit ledger entries and cannot access full audit exports or the audit API And unauthorized attempts are blocked (UI message and HTTP 403 for API) and all access attempts are logged with actor and timestamp
Preview Simulation and Approval Thresholds
"As a manager, I want to preview the rebalance impact and require approvals over certain thresholds so that large adjustments are controlled and reviewed."
Description

Allows authorized users to run a dry-run simulation of a rebalance before posting, showing impact by unit and payer with variance highlights. Supports configurable approval thresholds (e.g., total change or per-payer delta) that trigger required review and bulk approval. Includes rollback for the last posted rebalance batch to mitigate mistakes.

Acceptance Criteria
Authorized User Runs Dry-Run Rebalance Preview
Given a user with Rebalance:Simulate permission selects a billing cycle/date and split rules, When they click Preview, Then the system computes a dry-run using the same calculation engine as posting without mutating any ledgers. Given a community with up to 2,000 units and typical payer distributions, When a preview is run, Then the P95 completion time is 5 seconds or less. Given the preview completes, When results are displayed, Then each row shows unit ID, payer ID, payer name, pre-balance, post-balance, delta, resolution method (credit/refund/charge), and calculation timestamp. Given the preview completes, When the user navigates away and returns, Then the preview is available as Draft with a unique preview ID and unchanged results until it is re-simulated or posted. Given a user lacks Rebalance:Simulate permission, When they attempt to run a preview, Then access is denied and no computation occurs.
Variance Highlighting by Unit and Payer
Given a completed preview, When deltas are rendered, Then positive deltas are highlighted in red, negative in green, and zero in neutral gray consistently across table and export. Given a completed preview, When the user toggles display, Then deltas can be shown as absolute currency and as percentage of payer’s pre-balance. Given a completed preview, When the user sorts or filters by delta, Then the table updates accordingly and the filter persists during export. Given a completed preview, When totals are shown, Then the sum of all payer deltas equals the net change from included fees, credits, and refunds in the preview. Given currency rounding to two decimals, When deltas are computed, Then no per-payer rounding error exceeds $0.01 and the batch rounding reconciliation equals the negative sum of the individual rounding errors and is displayed.
Configurable Approval Threshold Evaluation and Routing
Given community-level approval settings defining total net change thresholds (absolute $ and % of monthly dues) and per-payer delta thresholds ($), When a preview completes, Then the system evaluates thresholds using absolute delta values. Given any configured threshold is exceeded, When the preview status is updated, Then it becomes Requires Approval and lists the triggered thresholds with actual vs threshold values. Given a preview Requires Approval, When it is created or re-simulated, Then all configured approvers receive in-app and email notifications within 60 seconds. Given no thresholds are exceeded, When the creator has Rebalance:Post-SelfApprove permission, Then they can approve and post without additional approver actions. Given thresholds are not configured, When a preview completes, Then the status defaults to Ready to Post if the user has Rebalance:Post permission.
Bulk Approval Workflow for Threshold-Triggered Batches
Given a preview has status Requires Approval, When a user with Rebalance:Approve permission opens it, Then they can Approve or Reject the entire batch. Given an approver Approves, When the action is saved, Then the preview status becomes Approved and posting is enabled for users with Rebalance:Post. Given an approver Rejects, When the action is saved, Then the preview status becomes Rejected and posting is blocked until a new preview is created. Given a preview is Approved, When underlying inputs (fees, credits, split rules, or payments) change, Then the preview is marked Stale and approval is cleared with a prompt to re-simulate. Given an approval action is taken, When the record is stored, Then the system captures approver, timestamp, action, and optional note for audit.
Posting Consistency and Idempotency from Preview
Given a preview with status Approved or Ready to Post, When the user clicks Post, Then the posted ledger entries match the preview’s per-payer deltas and resolution methods within rounding rules. Given a network retry or duplicate click, When the Post endpoint receives the same preview ID again, Then no duplicate batch is created and the response indicates Already Posted. Given the preview was generated against prior inputs, When a material input change is detected at post time, Then posting is blocked with Recalculate Required and no ledger changes are made. Given a batch is posted, When entries are created, Then each entry references the batch ID and payer notifications summarize the delta and reason. Given a batch is posted, When viewing the batch record, Then the system shows success status, counts of affected units/payers, and totals that equal the preview totals.
Rollback of Last Posted Rebalance Batch
Given one or more rebalance batches exist, When initiating a rollback, Then only the most recently posted rebalance batch can be selected for rollback. Given a user with Rebalance:Rollback permission confirms rollback, When the process runs, Then a compensating batch reverses all ledger entries from the target batch and restores pre-post balances. Given any entry from the target batch has been partially or fully settled by subsequent payments, When rollback is attempted, Then the rollback is blocked with a list of blocking transactions and no changes are made. Given a rollback succeeds, When the action is repeated, Then the operation is idempotent and no additional changes occur. Given a rollback is performed, When notifications and audit entries are created, Then the system records actor, timestamp, affected batch ID, and before/after totals.
Audit Trail and Export of Simulation and Approvals
Given a preview is created, approved/rejected, posted, or rolled back, When the event occurs, Then an immutable audit log entry is recorded with actor, timestamp, action, preview/batch ID, thresholds hit (if any), and before/after totals. Given a completed preview, When the user exports, Then a CSV is generated containing unit, payer, pre-balance, post-balance, delta, resolution method, and threshold flags that match the on-screen data. Given a posted batch, When the user downloads the Approval Report, Then it includes approver identity, approval timestamp, triggered thresholds, and the batch totals. Given audit permissions, When a user with Audit:View accesses the audit screen, Then they can filter by preview/batch ID and date range and view full event details. Given audit logs exist, When a user without Audit:View attempts access, Then access is denied and no audit data is exposed.
Resident Notifications and Feed Updates
"As a resident, I want to receive clear notifications when my share changes with a breakdown so that I understand what happened and avoid disputes."
Description

Publishes clear, itemized rebalance summaries to the Duesly feed and sends resident notifications via preferred channels with deep links to details. Bundles minor adjustments into digests to reduce notification fatigue, tracks read receipts, and logs delivery status. Honors communication preferences and roles while ensuring compliance messages are retained.

Acceptance Criteria
Feed Post: Itemized Rebalance Summary
Given a split rebalance completes for a unit When the system publishes the summary to the feed Then a new feed item is created within 60 seconds of completion And it displays per-resident adjustments (charge/credit), previous balance, new balance, and effective date And it shows the applied split rule name and billing cycle reference And it includes a deep link to the unit ledger details And it is visible only to authorized roles for that unit
Notifications: Preferred Channels with Deep Links
Given residents have notification preferences set per channel When a rebalance summary is published Then notifications are sent only via each resident’s enabled channels (email/SMS/push) And the message includes unit, net change, reason, effective date, and a deep link to details And no notification is sent to users without access to the unit And delivery logs capture channel, timestamp, and provider message ID per recipient
Digest: Bundle Minor Adjustments
Given multiple minor adjustments for the same unit occur within a 24-hour window and the board’s digest threshold is configured (default $5) When the system prepares notifications Then adjustments whose absolute values are <= threshold are bundled into a single digest per recipient And the digest lists each adjustment line item and total net change And individual notifications are suppressed for those adjustments And the feed still contains itemized posts for each rebalance
Delivery Status and Read Receipts
Given notifications are sent and a feed item exists When a resident opens the deep link or views the feed item Then a read receipt is recorded with resident ID, timestamp, and source (notification/feed) And per-channel delivery status is logged as sent, delivered, failed, or bounced with provider codes And read/unread status is reflected in the resident’s feed view
Role-Based Visibility and Preferences Enforcement
Given unit roles (owner, tenant, board) and resident communication preferences are configured When a rebalance summary is published Then only authorized roles can view the feed item and receive notifications And per-resident channel opt-outs and topic mutes are honored And non-compliance messages cannot bypass opt-outs via admin override And access attempts by unauthorized users return 403 without leaking content
Compliance Messages Retention and Immutability
Given a rebalance summary is tagged as compliance-relevant When it is published Then the feed item content is immutable; corrections are handled via append-only follow-up posts referencing the original And residents cannot delete the item; admins can only add corrections And the item and delivery/read logs are retained for at least 3 years or the configured retention period, whichever is longer And an audit trail captures publisher, timestamps, and any correction references
Deep Link Routing and Access Control
Given a resident clicks a deep link from a notification on mobile or desktop When the Duesly app is installed Then the link opens the rebalance details screen in-app; otherwise it opens the responsive web view And if the user is not authenticated, they are prompted to sign in before viewing details And if the user lacks permission, an access denied page is shown and no details are exposed And deep link URLs avoid PII and include time-bound tokens valid for up to 15 minutes

Guest Pay Pass

Create a time‑limited, amount‑capped payment pass for a one‑time contributor (e.g., a visiting parent or new roommate) without granting account access. Device‑bound, expiring links preserve privacy, tag the receipt to the guest, and keep the household on track without extra setup.

Requirements

Pass Creation & Configuration
"As a part‑time HOA manager, I want to quickly create a time‑limited, amount‑capped payment pass tied to a household so that a non-member can contribute without full account setup."
Description

Provide managers and board members with a guided flow to create a Guest Pay Pass from the Payments hub, a household profile, or directly from a feed post. The flow must capture: associated household/account, optional guest label (e.g., “Grandma Lopez”), total amount cap, expiration date/time, allowed payment methods (card, ACH), payer fee handling (absorbed vs passthrough), memo/notes, and linkage to an existing charge or category. Generate a unique pass ID and preview screen before issuing. On save, create a pass record with lifecycle states (Draft, Active, Expired, Revoked), persist audit metadata (creator, timestamps, source post), and surface a shareable secure link. Integrate with the feed by adding a pass-created activity item and by allowing one-click pass creation from any posted bill. Ensure accessibility, responsive UI, and role-based permissions (only authorized roles can create/modify passes).

Acceptance Criteria
Multi-Entry Creation Paths
Given a Manager or Board user is authenticated When they open the Payments hub Then a "Create Guest Pay Pass" action is visible and enabled Given a Manager or Board user is on a Household profile When they click "Create Guest Pay Pass" Then the creation flow opens with the household pre-selected Given a Manager or Board user is viewing a bill-type feed post When they click "Create Pass" Then the creation flow opens with linkage to the post's charge pre-filled Given the flow was launched from any entry point When the pass is issued Then audit metadata persists the source context (Payments hub, Household profile, or Feed Post with post ID)
Pass Field Capture & Validation
Given the pass creation flow is open When the user edits fields Then "Associated household/account" is required and must be selected before Preview/Issue enable And "Guest label" is optional (0–80 chars) And "Total amount cap" is required, numeric > 0, up to 2 decimal places, and uses community currency And "Expiration date/time" is required and must be in the future And at least one "Allowed payment method" (Card, ACH) must be selected And "Payer fee handling" requires an explicit choice (Absorbed or Passthrough) And "Memo/notes" is optional up to 500 characters And exactly one linkage is required: either to an existing charge or to a revenue category Given any invalid input (e.g., past expiration, missing household, no payment method, non-numeric amount) When the user clicks Preview or Issue Then the action is blocked and inline validation messages are shown next to the offending fields with no pass record created Given a payer fee handling selection When Passthrough is selected Then the Preview itemizes payer fees and shows the gross amount due from the payer Given a payer fee handling selection When Absorbed is selected Then the Preview omits payer fee line items and shows the net to the community reflecting absorbed fees
Preview and Unique Pass ID Generation
Given the form has valid inputs When the user clicks Preview Then a preview screen is displayed showing all configured details and a unique Pass ID And the Pass ID remains consistent for the session through Issue Given the user navigates back from Preview to edit and returns to Preview When they review the preview again Then the same Pass ID is displayed Given the user cancels from Preview without issuing When they exit the flow Then no pass record is persisted Given the user proceeds from Preview to Issue When the pass is created Then the persisted pass record uses the same Pass ID shown on the preview
Lifecycle States and Audit Trail
Given a user saves a pass as Draft from the creation flow When they view the pass details Then the state is Draft, no shareable link is available, and createdBy/createdAt are recorded Given a Draft pass exists When an authorized user issues it Then the state transitions to Active, issuedBy/issuedAt are recorded, and a shareable link is generated Given an Active pass reaches its expiration date/time When the system clock passes the expiration Then the state auto-transitions to Expired within 5 minutes and the shareable link is disabled Given an Active pass exists When an authorized user revokes it Then the state transitions to Revoked, revokedBy/revokedAt are recorded, and the link is immediately disabled Given any state change occurs When viewing the pass audit log Then a chronological list of lifecycle events (Draft, Active, Expired, Revoked) with timestamps, actors, and source context is displayed
Shareable Secure Link Behavior
Given an Active pass exists When the creator clicks "Copy Link" Then a secure HTTPS URL with a signed token is generated server-side and copied to the clipboard And the URL contains no household PII in its path or query parameters Given the secure link is opened while the pass is Active When the page loads Then the payment page displays the guest label, amount cap, allowed payment methods, and memo/notes exactly as configured Given the pass is Expired or Revoked When the secure link is opened Then an access-denied screen is shown indicating Expired or Revoked and no payment form is rendered Given multiple link openings by different users When the pass remains Active and unexpired Then each opening shows the same configured details and all payment attempts are attributed to the guest pass ID
Feed Integration and One-Click from Posted Bill
Given a pass is issued When the community feed is refreshed Then a "Guest Pay Pass created" activity item appears with the pass ID, associated household, creator, and timestamp, and links to pass details Given a posted bill exists in the feed When an authorized user clicks "Create Pass" on that bill Then the creation flow opens with the bill's charge pre-selected as the linkage and the household pre-filled And upon issuing, the feed activity item cross-references the originating bill/post Given a pass was created from a feed post When viewing that post Then an inline indicator shows that a Guest Pay Pass is associated with the post along with the pass state
Role-Based Permissions, Accessibility, and Responsive UI
Given a user without Manager or Board role (e.g., Resident) When they view the Payments hub, a household profile, or a bill post Then all Guest Pay Pass creation actions are hidden And direct API calls to create/modify/revoke a pass return HTTP 403 Given a Manager or Board user is authorized When they attempt to edit or revoke a pass they did not create Then the action is allowed only if their role has pass-modification permission; otherwise the UI disables the action and API returns HTTP 403 Given the creation flow is used with keyboard only When tabbing through controls Then focus order is logical, all controls are reachable, focus is visible, inputs have programmatically associated labels, and form errors are announced to screen readers Given the creation and preview screens are viewed at 375px width (mobile) and ≥1024px (desktop) When interacting with the UI Then layouts are responsive with no horizontal scrolling, dialogs fit the viewport, and touch targets are at least 44x44 px
Device‑Bound, Expiring Access Link
"As a security‑conscious board treasurer, I want the pass link to work only on the intended device and expire automatically so that sharing doesn’t expose household data or funds."
Description

Issue a cryptographically signed, time‑boxed URL for each Guest Pay Pass that binds to the first device that opens it. On first open, set a secure, httpOnly cookie and store a salted hash of a device fingerprint; subsequent access checks must validate device match and link validity (not expired/revoked, not consumed beyond cap). Provide a managed reissue flow for creators to resend or rotate the link, including optional OTP to a guest contact if provided. Support deep links for mobile and a branded web page for non-app users. Expired or revoked links must display a clear state page with contact instructions. Log all access events for audit while minimizing stored identifiers. Handle edge cases: multiple tabs, private browsing, cookie blocking, and provide a fallback verification step when binding cannot be established.

Acceptance Criteria
First Open Binds Link to Device
Given a newly issued, unbound Guest Pay Pass URL with a valid cryptographic signature and unexpired TTL When the guest opens the link for the first time on a device with cookies enabled Then the server verifies the signature, confirms the link is not expired or revoked, and that the cap is not consumed And sets a Secure, HttpOnly, SameSite=Lax cookie scoped to the pass domain with expiry not exceeding the pass TTL And computes and stores only a salted hash of the device fingerprint (no raw fingerprint persisted) And binds the pass to that device hash and cookie And responds with the access page And logs a 'first_open' audit event with pass_id, event_type, timestamp, and device_hash only
Subsequent Access From Bound Device Is Idempotent
Given a Guest Pay Pass link already bound to a device via cookie and stored device hash When the guest reopens the link on the same device/browser (including multiple tabs or refreshes) before expiry and before cap is consumed Then access is granted without re-binding or duplicate device records And concurrent tab opens do not create race conditions or multiple bind events And the existing cookie remains unchanged and valid And an 'access_allowed' audit event is logged with minimal identifiers
Unrecognized Device Access Triggers Fallback Verification
Given a Guest Pay Pass link bound to device A When the link is opened on device B (no matching cookie and device hash mismatch) or binding cannot be established due to blocked cookies/private browsing Then direct access is denied and no payment actions are exposed And a fallback verification flow is offered And if a guest contact is on file, a 6-digit OTP can be requested, is delivered via the selected channel, expires in 10 minutes, and is limited to 5 attempts with 30-second minimum retry And upon successful OTP verification, a temporary session is granted for the current browser session only (no persistent cookie), and access is allowed And the creator is prompted to reissue/rotate the link for durable access And events 'fallback_offered', 'otp_sent', 'otp_verified'/'otp_failed', and 'temporary_session_granted' are logged with minimal identifiers
Expired or Revoked Link Shows State Page
Given a Guest Pay Pass link that is expired by TTL or has been revoked by the creator When a guest opens the link Then a branded state page is displayed indicating 'Expired' or 'Revoked' with clear contact instructions and a 'Request New Link' action And no payment or account data is shown and no authenticated actions are available And an 'access_denied' audit event is logged with reason ('expired' or 'revoked') and minimal identifiers
Amount Cap and Consumption Enforcement
Given a Guest Pay Pass with a defined amount cap When the guest attempts to make a payment through the link Then the UI prevents entry of an amount exceeding the remaining cap And backend enforcement is atomic so concurrent attempts cannot exceed the cap And when cumulative successful payments reach the cap, the link is marked consumed and subsequent opens show a 'Consumed' state with no payment option And events 'payment_attempted', 'payment_completed', and 'cap_reached' are logged with minimal identifiers
Creator Reissue/Rotation With Optional OTP
Given the creator opens the pass management UI for a Guest Pay Pass When they select 'Reissue/Rotate Link' Then a new cryptographically signed URL is generated, previous URL is revoked immediately, and TTL is reset per policy And the creator can resend the new link via email/SMS if a guest contact exists And the creator can require OTP on next guest open And an audit log records 'reissued' with previous_link_id, new_link_id, timestamp, and actor_id And subsequent guest opens bind to the new link as per device-binding rules
Mobile Deep Link and Branded Web Page Routing
Given a valid Guest Pay Pass URL is opened on a mobile device When the Duesly app is installed Then the URL resolves via OS deep link into the app with parameters preserved server-side and device binding performed as specified When the app is not installed Then the URL opens a responsive, branded web page providing identical pass functionality and binding behavior And in all cases, the signed token is not leaked via Referer headers to third parties, and desktop browsers open the branded web experience And an 'open_routed' audit event is logged noting route ('app' or 'web') with minimal identifiers
Cap Enforcement & Payment Processing
"As a guest payer, I want to contribute an amount within the allowed limit and see how much remains so that I can pay confidently without errors or overcharges."
Description

Process guest payments against the pass using existing payment rails (card, ACH) while strictly enforcing the configured total cap. Display remaining allowance and prevent authorizations that would exceed it. Support partial payments until the cap is reached or the pass expires. If payer fees are configured as passthrough, include them in the presented total without affecting the cap logic; if absorbed, deduct net proceeds accordingly. Implement idempotent, concurrent‑safe decrements to prevent overspending across simultaneous attempts. Support network requirements (e.g., 3DS/SCA where applicable), handle declines and retries gracefully, and ensure settlement updates the pass balance and household ledger in near real time. Define refund rules so that approved refunds restore the remaining allowance while the pass is active. Provide clear success/failure receipts and post‑payment states.

Acceptance Criteria
Cap Enforcement at Authorization
- Given a guest pay pass with a total cap of $200 and a remaining allowance of $50, When a payer attempts to authorize a $60 payment by card, Then the system prevents submission and displays an error stating the amount exceeds the remaining allowance ($50) and no authorization request is sent to the processor. - Given a remaining allowance of $50, When a payer enters $50 exactly, Then the authorization request is submitted and, if approved, the remaining allowance is decremented by $50. - Given fees configured as passthrough, When validating the cap, Then the cap check applies to the pre‑fee principal only and prevents authorization if the principal would exceed the remaining allowance. - Given fees configured as absorbed, When validating the cap, Then the cap check applies to the payer’s entered amount and still decrements the remaining allowance by the full payer amount (principal), independent of processor fees.
Partial Payments and Expiration Handling
- Given a pass with a $200 cap, When a payer completes two payments of $80 and $120, Then both succeed and the remaining allowance becomes $0 and the pass status changes to Depleted. - Given a pass with $30 remaining, When a payer attempts a $40 payment, Then the system offers to adjust the payment to $30 (if UI supports quick-adjust) or blocks the attempt and displays the remaining allowance. - Given a pass with an expiration set to a future timestamp, When the expiration time is reached, Then further payment attempts are blocked with an Expired message and the remaining allowance is displayed as unchanged from pre‑expiry. - Given a pass not yet expired with $0 remaining, When a payer attempts any payment, Then the attempt is blocked with a Depleted message.
Fee Passthrough vs Absorbed Logic
- Given payer fees configured as passthrough at 3% + $0.30, When the payer enters a principal of $100, Then the presented total shows $103.30, the authorization is for $103.30, and the pass remaining allowance decrements by $100 only. - Given payer fees configured as absorbed at 3% + $0.30, When the payer enters a principal of $100, Then the presented total is $100, the pass remaining allowance decrements by $100, and the household ledger records net proceeds of $96.70 with fees recorded as expense. - Given fractional cents from fee math, When totals are computed, Then rounding is to the nearest cent using bankers’ rounding and cap comparisons are performed using exact decimal math such that the cap is never exceeded by $0.01 or more. - Given a switch of fee mode on an active pass, When a new payment is attempted, Then the latest fee mode is applied to presentation and cap logic without altering the historical ledger entries of prior payments.
Concurrent Safety and Idempotency
- Given a pass with $50 remaining, When two distinct devices submit $50 payments concurrently, Then at most one authorization succeeds and the other is rejected with a Remaining Allowance Changed message; no combined decrements exceed $50. - Given a client provides an idempotency key for a $25 payment, When the same request is retried within 24 hours with the same key, Then no duplicate charge is created and the identical response is returned. - Given network timeouts after the processor receives the request, When the client retries with the same idempotency key, Then only one charge is captured and the pass allowance is decremented once. - Given concurrent ACH and card attempts against the same remaining allowance, When processed, Then atomic decrements ensure the sum of authorized/captured principal never exceeds the allowance and rejected attempts clearly state the remaining allowance.
3DS/SCA Compliance
- Given a card and region requiring SCA, When the payer initiates payment, Then a 3DS challenge flow is triggered and upon successful completion the authorization proceeds without exceeding the cap. - Given the payer abandons or fails the 3DS challenge, When the session ends, Then the authorization is canceled and the pass remaining allowance is unchanged. - Given a card eligible for frictionless flow, When payment is initiated, Then the transaction proceeds without challenge while maintaining all cap checks and logging SCA exemption in the audit trail.
Declines and Retries Handling
- Given a soft decline (e.g., issuer unavailable), When the payer retries within the pass validity window, Then retries are allowed up to the processor’s recommended limits and the remaining allowance is unaffected until a successful authorization. - Given a hard decline (e.g., stolen card), When encountered, Then the system blocks further retries with the same instrument in that session and prompts the payer to use a different method; the remaining allowance is unchanged. - Given an ACH debit is submitted, When the ACH return code R01 (NSF) is received post‑submission, Then the household ledger entry is reversed and, if the pass is still active, the remaining allowance is restored by the principal amount. - Given any decline, When a receipt is generated, Then the failure receipt contains the decline reason code, timestamp, attempted amount, and pass state.
Settlement, Ledger Updates, Receipts, and Refunds
- Given a successful card capture, When the processor webhook is received, Then the pass remaining allowance and household ledger update within 30 seconds and the pass timeline shows the new balance. - Given an ACH payment, When it moves from submitted to settled, Then the ledger and pass remaining allowance reflect the principal at settlement time; if later returned, the ledger is reversed and allowance restored if the pass is active. - Given a successful payment, When the receipt is issued, Then the receipt shows payer total, principal, fees, pass ID, guest tag, last4/payment method, and post‑payment pass state (Active/Depleted/Expired/Pending ACH). - Given an approved refund on a still‑active pass, When processed, Then the refunded principal amount restores the pass remaining allowance up to the original cap and the ledger records a refund entry with linkage to the original payment. - Given an approved refund on an expired or depleted pass, When processed, Then the household ledger records the refund and the pass allowance is not restored; the receipt explains that allowance restoration applies only to active passes. - Given a partial refund, When processed, Then the allowance is restored by the refunded principal portion only (fees follow fee policy) and the new remaining allowance is displayed within 30 seconds of refund completion.
Guest Receipt Tagging & Household Ledger Mapping
"As a board secretary, I want guest payments to be clearly tagged and mapped to the correct household so that reporting and audits stay accurate without adding new user accounts."
Description

Tag every transaction originating from a Guest Pay Pass with the pass ID and guest label, attributing the financial impact to the associated household ledger without creating a user account. Update household statements to show guest‑sourced payments distinctly (e.g., “Guest: Grandma Lopez, Pass #P‑1234”) and allow filtering/export by source=guest for audits. Reflect pass balance and transaction history in a pass detail view accessible to creators and authorized roles. Include data in CSV exports and API responses, ensuring compatibility with existing accounting categories and reconciliation flows. Do not expose household feed or sensitive data to the guest; only the payer’s receipt page shows their own payment details.

Acceptance Criteria
Transaction Tagging & Ledger Attribution
Given an active Guest Pay Pass with ID "P-1234" labeled "Grandma Lopez" for Household "H1" and no user account exists for the guest When the guest pays $100.00 toward "2025 Q3 Dues" via the pass Then a transaction is created with source="guest", pass_id="P-1234", guest_label="Grandma Lopez", household_id="H1", category="Dues", payer_user_id=null And the household H1 outstanding balance is reduced by $100.00 within 5 seconds And no resident or guest user account is created for the payer And if the transaction is refunded or charged back, the resulting record retains source="guest", pass_id="P-1234", guest_label="Grandma Lopez" and links to the original
Statement Display of Guest Payments
Given a statement is generated (web and PDF) for Household "H1" for a period containing guest transaction "T1" from pass "P-1234" with label "Grandma Lopez" When the statement is rendered Then the payment line item displays "Guest: Grandma Lopez, Pass #P-1234" And the line item shows the transaction date, amount, and category And the payment is included in statement subtotals and totals And if the guest label is empty, the line item displays "Guest, Pass #P-1234" And currency, date, and locale formatting match the household settings
Guest Source Filter & CSV Export
Given a manager is viewing transactions for Household "H1" in the admin UI When filter "source=guest" is applied Then only transactions where source="guest" are listed and the count matches the header total When the user clicks "Export CSV" Then the CSV contains only the filtered rows and includes columns: source, pass_id, guest_label, household_id, transaction_id, amount, category, status, created_at And the sum of "amount" in the CSV equals the UI total for the filter
Pass Detail View: Balance & History Visibility
Given user "U1" is the creator of pass "P-1234" for Household "H1" When U1 opens the pass detail view Then the view shows cap_amount, total_collected, remaining_balance, status, and expires_at for P-1234 And it lists each transaction with transaction_id, date, amount, status (pending/settled/refunded), and payer reference (guest_label) And entries are sorted newest first and update within 5 seconds of new activity Given user "U2" is not authorized for H1 When U2 attempts to access the pass detail view for P-1234 Then access is denied with 403/404 and no pass metadata or household details are leaked
API Responses Include Guest Fields (Backward Compatible)
Given an API client requests GET /transactions?household_id=H1 When guest-sourced transactions exist Then each guest transaction in the response includes source="guest", pass_id, and guest_label fields And existing accounting fields (category_code, ledger_account_id, reconciliation_status) remain populated as before And clients that ignore unknown fields continue to parse responses without error When the client requests GET /transactions?household_id=H1&source=guest Then only guest-sourced transactions are returned
Guest Privacy & Scoped Receipt
Given a guest completes payment "T1" via pass link "P-1234" When the guest opens the receipt page from the payment confirmation on the same device Then the page displays only T1 details (amount, date/time, last4, pass_id, guest_label) and shows no household feed, other transactions, balances, resident names, or addresses beyond the household name And there are no links or API calls on the page that expose household feed or other members' data When the guest attempts to navigate to household or ledger URLs or call authenticated APIs Then the request is blocked with 401/403 and no sensitive data is returned
Reconciliation & Accounting Compatibility
Given a reconciliation run for period "P" includes resident payments and guest payments from passes P-1234 and P-5678 When generating the GL export and bank reconciliation reports Then guest transactions are included under existing accounting categories without creating new chart-of-accounts entries And debits equal credits for the period and totals match gateway settlement amounts within $0.01 And each guest transaction row includes pass_id and guest_label to support audit traceability And releasing the feature leaves historical period totals and category mappings unchanged
Automated Reminders & Expiration Handling
"As a busy volunteer board member, I want automated, logged reminders tied to the pass so that payments happen on time without me chasing people."
Description

Enable creators to configure automated reminders that nudge the guest and/or household contacts before expiration (e.g., 3 days before, day of expiry) and on failed payment attempts. Record all reminders in the activity log for compliance. Stop reminders automatically when the cap is reached or the pass is revoked. Support manual actions: extend/shorten expiration, pause reminders, and regenerate the access link. When a pass expires, mark it inactive, disable new payments, and notify the creator with a summary of outcomes. Display a friendly expired state to guests with guidance to contact the manager if payment is still needed.

Acceptance Criteria
Configurable Reminder Schedule and Audience
Given a guest pay pass with an expiration date and configured reminder offsets (e.g., 3 days before, day of expiry) and selected recipients (guest and/or household contacts) When each scheduled trigger time occurs before expiration Then the system sends exactly one reminder per trigger to the selected recipients including the pass title, amount due or remaining cap, and the current access link And no reminder is sent to unselected recipients And reminders scheduled after expiration are not sent
Reminders on Failed Payment Attempts
Given reminders are enabled for a guest pay pass And a payment attempt for that pass fails When the failure event is recorded Then a failure reminder is sent to the configured recipients and includes retry guidance and the current access link And exactly one reminder is sent per failed attempt event And if the cap is reached or the pass is revoked, no failure reminders are sent
Auto-stop Reminders on Cap Reached or Revocation
Given a guest pay pass with scheduled reminders When the total collected amount meets or exceeds the pass cap OR the pass is revoked by the creator Then all future scheduled reminders for the pass are canceled And no reminders scheduled at or after the stop time are delivered And the pass activity log records the auto-stop with the reason (cap reached or revoked) and timestamp
Manual Controls: Expiration Changes and Pause/Resume Behavior
Given an active guest pay pass with scheduled reminders When the creator extends the expiration date/time Then pending reminders are recalculated relative to the new expiration and only future reminders are (re)scheduled And the change and new schedule are recorded in the activity log When the creator shortens the expiration date/time Then reminders that would occur after the new expiration are canceled and expiration handling is applied at the new time When the creator pauses reminders Then no reminders are sent while paused When the creator resumes reminders Then only future reminders (after the resume time) are reinstated; missed reminders are not sent retroactively And each action (extend, shorten, pause, resume) is recorded in the activity log with before/after values and timestamp
Regenerate Access Link Behavior
Given a guest pay pass with an active access link When the creator regenerates the access link Then the previous link becomes invalid immediately and cannot be used to initiate or complete payments And a new unique link is generated and becomes device-bound on first successful open by the guest And all subsequent reminders reference the new link And the regeneration event and link version are recorded in the activity log
Expiration Handling, Creator Notification, and Guest Expired State
Given an active guest pay pass with expiration at time T When current time reaches or exceeds T and the cap has not been reached and the pass is not revoked Then the pass status changes to inactive/expired and new payments are blocked via UI and API And any reminders scheduled after expiration are canceled And the creator receives a notification summarizing outcomes: total collected, cap amount, percent of cap, count of successful payments, count of failed attempts, last activity timestamp, and a link to the pass And a guest visiting the link sees a friendly expired message with guidance to contact the manager if payment is still needed, and no payment UI is shown
Activity Log Completeness and Auditability
Given reminders and manual/system events occur for a guest pay pass When viewing the pass activity log Then every reminder (pre-expiry, day-of, failure-triggered) is recorded with timestamp, recipient role (guest/household), channel, trigger reason, template ID/name, delivery status, and link version And every manual action (extend, shorten, pause, resume, regenerate link, revoke) is recorded with actor, timestamp, and before/after values And each auto-stop (cap reached/revoked) and expiration transition is recorded with reason and timestamp And the activity log is immutable to non-admin users and exportable for compliance
Privacy & Data Minimization Controls
"As a privacy‑minded manager, I want to take a payment from a guest without exposing household data or retaining unnecessary personal information so that we stay compliant and respectful of privacy."
Description

Collect only essential guest data needed to process a payment (e.g., name, email/phone for receipt) and clearly disclose that no account will be created. Isolate guest interactions from the community feed and member directories. Store device identifiers as hashed, non‑reversible values and set retention limits for guest contact data (configurable, with receipts retained for compliance). Provide creators with controls to redact guest labels post‑payment and to export/delete guest contact info in accordance with privacy regulations. Ensure PCI DSS scope remains unchanged by keeping payment forms hosted by the PSP and never storing raw PANs.

Acceptance Criteria
Minimal Guest Data Collection & Disclosure at Guest Checkout
Given a guest opens a valid Guest Pay Pass, When the payment form loads, Then only Name and one contact field (Email or Phone) are displayed as required inputs and no other PII fields are present. Given a guest views the form, When before submission, Then a notice states that no account will be created and contact info is used only for receipts and pass-related communications. Given a guest completes payment, When records are created, Then no Duesly member account is created and only the provided contact and guest label are stored with the transaction and receipt. Given form telemetry and logs, When inspected, Then no additional PII beyond Name and Email/Phone is stored or logged by Duesly.
Guest Interaction Isolation from Community Surfaces
Given a Guest Pay Pass payment is created or completed, When viewing the community feed and member directories, Then the guest does not appear and no guest activity is posted to the feed. Given the household ledger, When the payment is recorded, Then it is tagged as Guest and visible to authorized household members and admins only. Given guest links are shared, When accessed, Then guests cannot navigate to community content or member profiles.
Hashed, Non‑Reversible Device Identifier Storage
Given a guest opens a pass, When a device identifier is captured, Then Duesly stores only a non-reversible hash and does not store the raw identifier in databases or logs. Given the stored identifier, When attempting to retrieve the original value, Then it cannot be derived by Duesly systems or support tools. Given multi-tenant environments, When comparing hashed identifiers across communities, Then values do not allow cross-community correlation of a single guest device.
Configurable Retention and Purge of Guest Contact Data
Given an admin sets a retention period (e.g., 30–365 days), When the period elapses, Then guest contact data (name, email/phone) associated to the pass is automatically purged or anonymized while receipts and transaction records are retained. Given scheduled jobs, When the purge job runs, Then a system audit log records the count of records purged and the job outcome. Given backups, When retention is applied, Then purged guest contact data is excluded from restored backups within the documented restoration window.
Creator Controls to Redact, Export, and Delete Guest Info
Given a completed guest payment, When the creator selects Redact guest label, Then the guest label on the ledger/feed is replaced with a generic label (e.g., Guest) without altering the receipt details. Given a guest contact record, When the creator requests Export, Then a machine-readable file (CSV/JSON) containing only the guest’s contact data and related pass metadata is generated within 5 minutes. Given a guest contact record, When the creator requests Delete, Then the guest contact data is removed and an audit entry is created, while receipts and transaction data remain intact.
PSP‑Hosted Payment and No Raw PAN Storage
Given a guest initiates payment, When entering card details, Then all card data fields are rendered and hosted by the payment service provider and not within Duesly’s domain or DOM. Given network and application logs, When reviewed, Then no raw PAN, CVV, or expiry values are transmitted to or stored by Duesly systems or support tooling. Given the payment integration, When validated, Then disabling the PSP form prevents card entry on Duesly pages, confirming no fallback collects PAN on Duesly.
Security, Abuse Prevention & Revocation
"As an admin, I want controls to revoke compromised passes and safeguards against abuse so that our community’s payments remain secure and trustworthy."
Description

Protect Guest Pay Passes with rate limiting on link access and payment attempts, bot detection (e.g., invisible challenge), CSRF protections, and signed URL verification. Allow creators and admins to revoke a pass immediately, forcing token rotation and invalidating existing device bindings. Emit security events (suspicious access locations, excessive failures) to monitoring and optionally notify creators. Maintain a full audit trail of pass lifecycle changes and payment attempts. Provide configuration for optional geo‑fencing by country/region if required by the HOA. Ensure all endpoints enforce RBAC, transport security, and secure cookie flags. Include disaster recovery considerations so that revoked/expired state persists across cache resets.

Acceptance Criteria
Rate Limiting and Abuse Throttling on Guest Pay Pass
Given any client requests the Guest Pay Pass URL When the same IP makes more than 20 GET requests within 60 seconds Then subsequent requests within that window return HTTP 429 with a Retry-After header set to the remaining seconds. Given a device attempts payment When there are 5 failed authorizations within 10 minutes for the same pass Then further payment attempts are blocked for 15 minutes and return HTTP 423 Locked. Given distributed abuse is detected (≥50 requests/min across ≥10 IPs targeting the same pass) When triggered Then a global rate limit of 100 requests/minute per pass is applied for 15 minutes.
Bot Detection and Invisible Challenge Enforcement
Given headless indicators or abnormal interaction patterns are detected (e.g., no pointer events, zero dwell time, automation signals) When the pass page is loaded Then an invisible challenge is executed and must succeed before the payment form is delivered. Given ≥10 distinct device fingerprints access the same pass within 5 minutes When the 11th access occurs Then a bot challenge is enforced and failure results in HTTP 403 with no payment form rendered. Given a legitimate user accesses up to 3 times within 1 minute on the same device and passes challenges When proceeding to payment Then time-to-interactive increases by ≤200 ms and no explicit CAPTCHA UI is shown.
Signed URL Verification and Device Binding
Given a Guest Pay Pass URL with embedded expiry and HMAC signature When the current time exceeds the expiry or the signature does not verify Then the URL is rejected with HTTP 403 and a security event is logged. Given the first successful access from a device When device binding is created Then subsequent access from a different device is denied by default with HTTP 403 unless policy allows multi-device access. Given the same signed URL is replayed with a different IP/UA within 60 seconds When device binding does not match Then the request is rejected with HTTP 403 and counted as a suspicious event. Given a valid request from the bound device within the pass validity window When accessed Then the payment form renders successfully.
Immediate Pass Revocation and Token Rotation with DR Persistence
Given a creator or HOA admin clicks Revoke on an active pass When confirmation is submitted Then all tokens and device bindings for the pass are invalidated and rotated within ≤5 seconds, and the pass status becomes Revoked. Given any revoked pass is accessed When a GET or POST occurs Then the system responds with HTTP 410 Gone and no payment is processed. Given cache reset or infrastructure failover occurs When services restart Then the revoked/expired state remains enforced and cannot be bypassed via stale caches.
Security Events Emission, Notifications, and Audit Trail
Given suspicious access (e.g., >5 4xx responses in 5 minutes for a pass, signature failures, new country access) When detected Then a security event with passId, reason, severity, ip, country, userAgent, and timestamp is emitted to monitoring within 5 seconds. Given the creator has alerts enabled When a High severity event occurs Then an email notification is sent within 2 minutes containing a summary and recommended actions; otherwise, no email is sent. Given any lifecycle change (create, update, revoke, expire) or payment attempt (success/failure) When it occurs Then an immutable audit record is persisted with fields: passId, action, actorId/role, timestamp (UTC), ip, userAgent, geo, result, correlationId, and is retrievable by authorized admins within ≤2 seconds for the last 90 days.
Geo-fencing Enforcement by Country/Region
Given geo-fencing is enabled with an allowlist of countries/regions When access originates from a location not in the allowlist Then access is blocked with HTTP 451 and the attempt is logged to security and audit. Given geolocation cannot be determined with confidence ≥0.8 When access occurs Then the system fails closed and blocks access with HTTP 451 unless the pass has "allow unknown" explicitly enabled. Given the allowlist is updated by an admin When the next access occurs Then the new policy takes effect within ≤60 seconds.
RBAC Enforcement, Transport Security, CSRF, and Secure Cookies
Given an API call to create, revoke, or view a Guest Pay Pass When the caller lacks the required role (creator or HOA admin) Then the request is denied with HTTP 403 and the attempt is logged. Given any endpoint handling Guest Pay Pass or payment When accessed over non-TLS Then the request is redirected to HTTPS and HSTS is enforced; mixed-content requests are blocked. Given the payment form is submitted When the CSRF token is missing or invalid, or cookie constraints are not met (Secure, HttpOnly, SameSite per config) Then the submission is rejected with HTTP 403 and no side effects occur.

Household Statement

A clean monthly roll‑up that shows total due, who paid what, what remains, any fees, and plan status by co‑payer. Shareable as PDF/CSV for landlords, accountants, or reimbursement needs, it ends “who covered this?” threads and builds trust inside the household.

Requirements

Monthly Statement Calculation Engine
"As a household member, I want a monthly statement that clearly totals what we owe and breaks down who paid what so that I can see what remains and avoid confusion in our household."
Description

Implements the core logic to compile a monthly household roll-up from the billing ledger, including charges, payments, credits, refunds, late/processing fees, and adjustments. Calculates total due, total paid, remaining balance, and per-payer contributions, with clear attribution for each co-payer and support for partial/split payments across multiple items. Handles payment plans by reflecting current period obligation, paid-to-date, missed installments, and plan status per co-payer. Ensures correct period boundaries (configurable close date/timezone), idempotent regeneration for the same period, and versioning when post-close adjustments occur (with visible revision notes). Integrates with existing Duesly billing events (including posts converted to bills) and payment rails to ensure real-time accuracy. Provides consistent currency/locale formatting and rounding rules, and gracefully handles no-activity months. Includes performance targets to render under 2s for 95th percentile households and robust error reporting when data is incomplete.

Acceptance Criteria
Monthly Period Boundary and Idempotent Regeneration
- Given a configured close day-of-month and timezone for a household, when generating the statement for a period, then ledger entries with posting_timestamp in [period_start_tz, period_close_tz) are included; those >= period_close_tz are excluded. - When the timezone or close day is changed and the statement is regenerated for the same calendar month, then included entries shift according to the new boundary rules. - When regenerating the same period with identical inputs (household, close rules, ledger state snapshot), then the statement_id and content_hash remain identical, and all totals and line items are equal byte-for-byte. - Line items are deterministically ordered by posting_timestamp ASC, then by stable_id ASC. - Entries exactly at period_start_tz 00:00:00 are included; entries exactly at period_close_tz are excluded.
Totals Calculation and Per-Payer Attribution
- Total_Charges = sum of charges and fees (late, processing) within the period; Total_Payments = sum of payments and credits within the period (credits reduce balance); Total_Refunds = refunds within the period (increase balance); Total_Adjustments = sum of adjustments with sign (+/-) applied. - Total_Due = Opening_Balance + Total_Charges + Total_Adjustments - Total_Payments + Total_Refunds; Remaining_Balance = Total_Due; Total_Paid = (Total_Payments - Total_Refunds) displayed as a positive value. - For each co-payer, contributions equal amounts they authorized/paid, including their portion of split payments; household totals equal the sum of co-payer contributions within 0.01 minor units. - Amounts use ISO-4217 minor units; display rounding is half away from zero to the currency’s minor unit precision; internal arithmetic uses >= 4 decimal places; rounding occurs only at display/export. - Locale/currency formatting matches household/community settings (e.g., fr-FR shows 1a0234,56a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0€). - If no ledger activity occurs in the period, the statement shows "No activity this period", Opening_Balance is carried forward, and totals reconcile with the prior closing balance. - Charges + Adjustments + Opening_Balance - Payments + Refunds = Remaining_Balance within 0.01 minor units.
Partial and Split Payment Allocation Across Items
- Given a payment that references multiple items, when allocation rules are applied, then the engine allocates by item due date ASC, then by item_id ASC, until the payment amount is exhausted. - Given a payment without explicit allocation, when applied, then the engine allocates FIFO by oldest unpaid charge/fee first. - Allocation records are persisted with item_id, payer_id, and amount_allocated, and appear in the statement so each payment line references its covered items. - Per-payer contribution equals the sum of allocations attributed to that payer across all items; household totals equal the sum of per-payer contributions. - Partial allocations leave residual unpaid amounts on items, which appear in Remaining_Balance and plan calculations. - Duplicate payment events with the same external_event_id are ignored (idempotency), and allocations are not duplicated.
Payment Plan Obligation and Status Per Co-Payer
- For each active plan and co-payer, Current_Period_Obligation equals the scheduled installment whose due_at falls within [period_start_tz, period_close_tz); if plan is paused, obligation is 0. - Paid_To_Date equals the sum of allocations to plan installments with due_at <= period_close_tz. - Missed_Installments are any installments with due_at < period_close_tz and unpaid_amount > 0.01 minor units. - Status per co-payer is derived as: Completed if all installments paid; On Track if no missed installments and at least one future installment remains; Delinquent if any missed installment exists; Paused if plan.paused = true. - Split plans among co-payers attribute obligations and payments per co-payer share; the sum of co-payer obligations equals the plan’s installment amount. - Statement displays for each co-payer: Current_Period_Obligation, Paid_To_Date, Missed_Installments count, and Status.
Post-Close Adjustments, Versioning, and Revision Notes
- When any ledger entry affecting a closed period is added, updated, or deleted, then a new statement version is created for that period with version incremented by +1 (e.g., v1 -> v2). - Each version stores and shows a stable content_hash and human-readable Revision_Notes summarizing the delta (e.g., “Added late fee of $25 on 2025-08-02”). - Prior versions remain immutable and retrievable; exports are labeled with period and version (e.g., 2025-07_v2). - Regenerating a version without further ledger changes yields the same content_hash. - The current version is flagged as latest by default for sharing; previous versions are selectable.
Real-Time Integration with Billing Events and Payment Rails
- New billing events (post-to-bill, charge, fee, payment, credit, refund, adjustment) with unique external_event_id are ingested and reflected in a regenerated statement within 5 seconds p95. - Duplicate events (same external_event_id) do not change totals or line items (idempotent processing). - Out-of-order events are processed using posting_timestamp for inclusion; correctness is achieved upon next regeneration, and the statement aligns with the ledger state. - Bills originating from posts include a reference to the source post_id on corresponding line items. - When a payment or refund is reversed, the reversal event negates the original as a distinct line item; totals reconcile accordingly.
Performance and Error Reporting
- Statement generation latency is <= 2,000 ms at the 95th percentile measured server-side across representative 95th-percentile households (<=5 co-payers, <=300 ledger events in period, <=2 active plans). - p99 latency is <= 5,000 ms for the same dataset profile. - On incomplete or invalid data (e.g., missing timezone, unknown currency, corrupted ledger link), the engine returns a structured error with code prefix STMT_CALC_*, appropriate HTTP 4xx/5xx, a user-facing message, and a correlation_id; no partial totals are persisted. - All errors and generation attempts emit metrics: duration, success/failure, household_id, version_id (if applicable), and content_hash (on success). - Timeouts trigger a retry up to 2 times with jittered backoff, then surface STMT_CALC_TIMEOUT.
Co-payer Attribution & Plan Status
"As a co-payer, I want my payments and plan status clearly attributed to me so that everyone can see who covered what and coordinate remaining obligations."
Description

Maps each transaction to an identified co-payer using payer identity, payment method, or admin assignment, with the ability for authorized users to manually adjust attribution while preserving an audit trail. Displays per-payer subtotals, contribution percentages, and current payment plan status (on track, at risk, missed) alongside any fees applied to that payer. Supports guest payments and bank/card changes while maintaining consistent identity. Provides configurable allocation rules for ambiguous payments (e.g., first-in-first-out by due date, evenly split, or plan-first), and surfaces attribution confidence. Ensures changes to attribution immediately reflow statement totals and are reflected in exports and historical versions.

Acceptance Criteria
Auto Attribution via Payer Identity and Payment Method
Given a posted payment with a payer account ID matching a co-payer profile When the payment is ingested Then attribute the transaction to that co-payer and set confidence to High Given a payment from a saved bank/card whose fingerprint maps to a co-payer When the payment is ingested without an active session Then attribute to that co-payer and persist the method fingerprint on the attribution record Given conflicting identity signals (e.g., login ID for Payer A but card fingerprint for Payer B) When the payment is ingested Then apply the configured precedence (default: AccountID over Method) and set confidence to Medium and flag Requires Review
Manual Re-attribution with Audit Trail and Immediate Reflow
Given an authorized user with role {Admin, Manager} When they change the payer on a transaction Then an audit entry is created capturing previous payer, new payer, actor, timestamp, reason, and request ID Given a transaction's payer is changed When the statement view is open Then per-payer subtotals, contribution percentages, fees, and household totals update within 1 second without stale values Given an attribution change has been saved When a PDF or CSV export is generated Then the export reflects the new attribution and previous exports remain retrievable and unchanged
Per-Payer Subtotals, Fees, and Plan Status Display
Given a household statement with two or more co-payers When the statement is rendered Then each co-payer section displays: subtotal paid, fees applied to that payer, contribution percentage rounded to 0.1%, and plan status in {On Track, At Risk, Missed} Given plan status rules (On Track: no overdue installments; At Risk: next installment due within 7 days with insufficient paid toward plan; Missed: at least one overdue installment) When the statement is calculated Then the correct status badge and next-due/overdue count are shown per payer Given the statement totals are calculated When comparing sum(per-payer subtotals + fees) to the household total Then the difference is <= $0.01 USD
Guest Payments and Method Changes Maintain Payer Identity
Given a guest payment where the payer enters an email or phone matching an existing co-payer When the payment is ingested Then attribute to that co-payer, set confidence to Medium, and mark payment source as Guest Given a payment method token is replaced for the same underlying card/bank When the next payment is received Then the new method auto-links to the existing co-payer and confidence remains High Given no identity signal meets the confidence threshold When the payment is ingested Then route the item to Unattributed, apply the household's ambiguous allocation rule, and place the transaction in the Review queue
Configurable Allocation Rules for Ambiguous Payments
Given an ambiguous payment and allocation rule = FIFO by Due Date When attribution runs Then allocate amounts to the earliest unpaid obligations across co-payers until the payment is exhausted and set confidence to Low Given allocation rule = Even Split When attribution runs Then split the amount evenly among co-payers with remaining balances, rounding to cents and assigning any remainder to the payer with the largest remaining balance Given allocation rule = Plan First When attribution runs Then allocate to plan installments before fees or ad-hoc charges for each co-payer Given an admin changes the allocation rule at the household level When a new ambiguous payment arrives Then the new rule is applied and prior allocations remain unchanged with the rule used recorded in audit
Attribution Confidence Surfaced and Actionable
Given a transaction with attribution confidence in {High, Medium, Low} When the statement is rendered Then a confidence indicator is displayed with a tooltip explaining signal sources and any conflicts Given a transaction marked Low confidence When the user filters by Review Needed Then the transaction appears and can be confirmed or reassigned Given a user confirms a Low or Medium confidence attribution When saved Then the confidence becomes Confirmed and the item is removed from the Review queue
Historical Versions and Immediate Reflow Consistency
Given a change to a transaction's payer attribution When the change is saved Then the current statement view and totals reflow immediately and reflect the change in under 1 second Given statements are versioned When retrieving a prior version by timestamp Then values reflect the state at that time including attribution, plan statuses, and fees, and include a link to the related audit entries Given a prior version is exported to PDF/CSV When compared to the on-screen prior version Then all amounts, per-payer allocations, statuses, and confidence values match exactly
Scheduled Statement Generation & Delivery
"As a household admin, I want statements delivered automatically each month so that our household consistently has an up-to-date view without manual effort."
Description

Automatically generates and publishes the household statement on a configurable monthly schedule with timezone support. Posts the statement to the Duesly feed, notifies household members via in-app and email notifications, and logs delivery outcomes. Supports preview-and-approve workflows for managers, immediate reissue for corrections (with versioning), and ad-hoc on-demand generation. Integrates with reminder automation to trigger payment reminders when a statement shows an outstanding balance. Provides fail-safes for months with no activity ("No charges this period") and retries on transient failures.

Acceptance Criteria
Timezone-Aware Scheduled Monthly Generation
Given a community has configured a monthly statement schedule with day, local time, and IANA timezone When the scheduled local time occurs (including during DST transitions) Then the system generates one statement per active household for the configured period within 5 minutes And each statement is timestamped using the configured timezone And duplicate job triggers do not create duplicate statements (idempotent run) And households without configuration are skipped and logged
Feed Publication and Multi-Channel Notifications
Given a statement is successfully generated When it is published Then a feed post is created for the household with summary, and downloadable PDF and CSV And the post is visible to the household members and community managers And in-app notifications are sent to all active household members And email notifications are sent to all members with email enabled And notifications are delivered within 2 minutes of publication And the email subject includes community name and statement period
Retries on Transient Failures with Outcome Logging
Given a publish or notification attempt encounters a transient error (e.g., timeout or 5xx) When the system processes the delivery Then it retries up to 3 times with exponential backoff And no duplicate feed posts or duplicate emails are created across retries And upon final failure, the delivery is marked Failed and a manager alert is generated And all attempts are logged with timestamp, channel, attempt count, and result in the audit log And successful outcomes are logged with correlation IDs
Manager Preview-and-Approve Workflow
Given preview-and-approve is enabled for the community When scheduled statements are generated Then they are created as Draft and not visible to households And managers can view PDF/CSV, edit message, and approve or reject And Approve publishes immediately and logs actor, timestamp, and version And Reject prevents publication and logs reason And if an auto-publish policy is configured, the system follows it at the scheduled time
Immediate Reissue with Versioning
Given a previously published statement requires correction When a manager selects Reissue and confirms Then the system creates a new version (v2, v3, …) with updated totals and content And the latest version supersedes prior in the feed while preserving access to prior versions for admins And household default view shows only the latest version with a Version label And in-app and email notifications are sent for the reissue And audit log records previous version, new version, actor, reason, and timestamp
Ad‑Hoc On‑Demand Statement Generation
Given a manager initiates on-demand generation for a selected household and period When the request is submitted Then the system generates the statement using the current ledger snapshot And the manager can choose Publish now or Save as Draft And if an identical period was already published, the system requires Force reissue or aborts And notifications, feed posting, and logging follow the same rules as scheduled runs
Reminder Automation and No‑Activity Safeguard
Given a statement is published When the statement balance is greater than $0 Then the reminder automation is created/updated per the configured cadence starting from publish time And duplicate reminder schedules are not created for the same statement version When the statement balance is $0 or there were no charges this period Then no payment reminders are scheduled And the statement displays a No charges this period indicator And if a reissue changes the balance, reminders are created, updated, or canceled accordingly
PDF/CSV Export & Shareable Links
"As a tenant, I want to export or securely share my household statement as a PDF or CSV so that I can submit it to my landlord or accountant for reimbursement or records."
Description

Generates a clean, branded PDF and structured CSV of the monthly statement showing totals, per-payer breakdowns, itemized charges, fees, and plan statuses. Offers one-click export from the statement view and the feed post. Provides secure shareable links for landlords/accountants with optional expiration, password protection, and watermarking; links display a read-only web view matching the PDF. Supports locale-aware formatting, selectable date ranges, and redaction of sensitive identifiers (e.g., bank last4). Ensures exports are consistent with the on-screen statement version and logs download/access events for audit.

Acceptance Criteria
One-Click PDF/CSV Export from Statement View
Given I am viewing a household's monthly statement, When I click Export > PDF, Then a branded PDF is generated and downloaded containing totals, per-payer breakdowns, itemized charges, fees, plan statuses, statement period, and community branding (name/logo). Given I click Export > CSV, Then a CSV is generated and downloaded with a header row and columns covering: statement_period, entry_date, entry_type (charge/payment/fee/adjustment), description, payer_name, payer_id, plan_status, amount, balance_after, and household_id. Then for both PDF and CSV, all totals, balances, and line items exactly match the on-screen statement version at the time of export. And the exported files include a generated timestamp, page numbers for PDF, and are named with household identifier and period (e.g., duesly-statement-<household>-<YYYY-MM>).
One-Click Export from Feed Post
Given I am viewing the corresponding statement post in the community feed, When I click Export and choose PDF or CSV, Then the same outputs are produced without navigating away and reflect the underlying statement for that period. Then permission checks ensure only authorized household members and board/managers can export; unauthorized users see no export controls. And the results (content and totals) are identical to exports triggered from the statement view for the same statement and time.
Shareable Links: Read-Only Web View with Expiration, Password, and Watermark Matching PDF
Given I choose Share Link from the statement, When I set an optional expiration date/time and optional password and enable watermarking, Then a unique secure URL is generated with the selected options stored. When the link is opened before expiration without the required password, Then access is denied with no statement data shown; When the correct password is supplied, Then the statement is displayed. When the link is opened after expiration, Then an expiration message is shown and no statement data is returned. Then the read-only web view displays the same content, totals, per-payer breakdowns, itemized charges, fees, and plan statuses as the exported PDF for the selected date range. And the watermark is visible across all pages/sections of the web view and prints with the page; it includes at least the community name and a "Shared" indicator. And the web view presents no edit or payment actions (read-only).
Locale-Aware Formatting and CSV Structure
Given the user's or community's locale is set, When exporting PDF or viewing via shareable link, Then currency symbols, thousand/decimal separators, negative amounts, and date formats are rendered per locale. When exporting CSV, Then dates are ISO 8601 (YYYY-MM-DD), numeric fields use '.' as the decimal separator, and no locale-specific currency symbols are included to preserve machine readability. Then CSV is RFC 4180 compliant (UTF-8, comma delimiter, quoted fields as needed, CRLF line endings) and includes a single header row.
Selectable Date Range Export
Given a date range picker is present on the statement, When I select a custom start and end date and choose Export, Then the generated PDF/CSV include only entries within the inclusive date range and recompute totals and balances accordingly. Then the date range selection is validated so the end date cannot precede the start date; invalid inputs are rejected with an inline error and no export occurs. And the export filename and statement headers reflect the selected date range.
Redaction of Sensitive Identifiers
Given a Redact sensitive identifiers option is available, When redaction is enabled for an export or shareable link, Then all sensitive identifiers (e.g., bank account last4, internal account IDs) are masked (e.g., ****) in PDF, CSV, and web view. Then full bank/account numbers never appear in any export or web view; without redaction enabled, at most last4 may be shown where applicable. And for newly created shareable links, redaction is enabled by default and is indicated in the UI before sharing.
Audit Logging of Exports and Link Access
Given any export (PDF/CSV) is generated, When the export completes, Then an audit event is recorded with actor_id, household_id, statement_period or date_range, export_type (PDF/CSV), timestamp, and file_name. Given a shareable link is created, When it is accessed (successfully or denied), Then an audit event is recorded with link_id, access_result (success/denied/expired), timestamp, and remote_ip/user_agent when available. Then authorized admins can view these audit records for the household to satisfy audit requirements.
Inline Annotations & Dispute Resolution
"As a co-payer, I want to annotate specific items on the statement so that we can quickly resolve any confusion about who paid what and why fees were applied."
Description

Allows authorized household members to add comments, @-mentions, and attachments on specific statement line items to resolve questions like "who covered this?" without leaving the statement. Supports marking items as "disputed" or "clarified" with simple statuses and timestamps, and links back to the originating bill post for full context. Maintains a read-only ledger while capturing discussion and outcomes as metadata, which is reflected in subsequent statement versions and included in exports where applicable.

Acceptance Criteria
Inline Comment with @-Mention on Statement Line Item
Given an authorized household member is viewing a household statement with line items When they add an inline comment on a specific line item containing an @-mention of another household member Then the comment is saved and rendered beneath that line item with author name, ISO-8601 timestamp, and preserved formatting And the @-mention displays as a tagged user with correct display name and profile link And the line item shows an incremented comment count badge And no ledger amounts or balances are altered.
Mark Line Item as Disputed
Given an authorized household member opens the actions for a statement line item When they set the item status to Disputed and confirm Then the item displays a Disputed status pill with the actor’s name and ISO-8601 timestamp And the status is recorded as metadata on that line item And the CSV/PDF exports for that statement include dispute_status=Disputed and dispute_updated_at values for that line.
Resolve Dispute as Clarified with Resolution Note
Given a statement line item is in Disputed status When an authorized household member changes the status to Clarified and adds an optional resolution note Then the status updates to Clarified with actor name and ISO-8601 timestamp And the resolution note is saved and displayed beneath the item And subsequent versions of the statement reflect the Clarified status and note And financial ledger values remain read-only and unchanged.
Link to Originating Bill Post for Context
Given a statement line item originated from a bill post When a user clicks View Source on that line item Then the app opens the originating bill post in the community feed within the same account and household context And the user sees the full post content and payment history if permitted, or an access denied message if not And navigating back returns the user to the same statement and line item anchor.
Annotations and Status Metadata in Subsequent Statements and Exports
Given a line item received annotations and/or status changes in the current statement cycle When the next monthly statement is generated Then the new statement displays a summary badge per line item with annotation_count and current dispute_status And the CSV and PDF exports include columns annotation_count, dispute_status, last_annotation_at, and last_status_actor with accurate values And exported metadata matches in-app data for the same statement snapshot.
Attachments on Line Item Annotations
Given an authorized household member attaches files to an inline comment on a statement line item When they upload a supported file and post the comment Then the attachment is stored and displayed as a clickable thumbnail or filename with size And clicking the attachment opens a secure preview or download link And the CSV export includes an attachments column with secure URLs or identifiers for each attachment, where applicable And unsupported file types or files exceeding the configured size limit are rejected with a clear error without saving the comment.
Authorization and Visibility Controls for Annotations
Given a user without annotate permission views a household statement When they open a line item with annotations Then they can read existing comments, attachments, and statuses And they cannot create, edit, or delete annotations or change statuses; related controls are disabled or hidden And attempts to mutate annotations via API are denied with HTTP 403 and no data change.
Access Control & Audit Logging
"As a household member, I want tight control over who can access our statement and a clear audit trail so that I feel confident our financial details are private and trustworthy."
Description

Enforces fine-grained permissions so only household members and authorized community managers can view statements; external viewers require explicit share links with scoped access. Implements link expiry, revocation, and optional two-factor confirmation for sensitive views. Captures a detailed audit log of statement generation, attribution changes, exports, shares, and external access events, with user, timestamp, and action. Ensures data security via encrypted storage and transport, adheres to data retention policies, and supports household-level privacy settings to build trust.

Acceptance Criteria
Household and Manager Access Enforcement
Given an authenticated user who is a member of household H When they request to view statement S belonging to H Then the system returns S with HTTP 200 and displays all permitted fields Given an authenticated user who is not a member of H and does not have "View Statements" permission for community C that includes H When they request S Then the system returns HTTP 403 with no statement metadata leaked Given an authenticated community manager with "View Statements" permission for community C that includes H When they request S Then the system returns S with HTTP 200; otherwise the system returns HTTP 403 Given any attempt to access S (allowed or denied) When the attempt is processed Then an audit log entry is recorded with {actor/user id, household id, statement id, action 'view_attempt', outcome 'allowed'|'denied', timestamp}
Scoped Share Link Creation, Expiry, and Revocation
Given an authorized household member or manager When they create a share link for statement S Then a unique, single-purpose tokenized URL is generated that grants read-only access to S for household H and no other resource Given a newly created share link with expiry datetime E (default per policy) When now > E Then link access returns HTTP 410 or 403 and the event is logged Given an existing share link When the creator revokes the link Then subsequent requests via that link are immediately invalidated (HTTP 410 or 403) and the revocation is logged Given share link token generation When the token is created Then the token has at least 128 bits of entropy and is excluded from server logs; the URL contains no PII Given "single active link" is enabled for S When a new link is generated Then all prior links for S are invalidated
External Viewer Access with Optional 2FA
Given a valid, unexpired share link with 2FA required When the external viewer submits a correct one-time code delivered to the configured channel Then the system grants read-only access to S for the current session and logs the success Given a valid, unexpired share link with 2FA required When the external viewer submits an incorrect code 5 times within 10 minutes Then the system denies access, rate-limits further attempts for at least 15 minutes, and logs the failures Given a valid, unexpired share link with 2FA not required When the link is opened Then the system grants read-only access to S and logs the access Given an external viewer session via a share link When they attempt to access statements or data outside the link scope Then the system denies access (HTTP 403) and logs the attempt
Statement Generation and Attribution Change Auditing
Given a statement S is generated (manual or scheduled) When generation completes Then an audit log entry is created with {actor id or system id, action 'statement_generated', household id, statement id, timestamp (UTC)} Given any payer attribution field on S is added, updated, or removed When the change is saved Then an audit log entry is created with {actor id, action 'attribution_changed', household id, statement id, changed fields list (names only), timestamp} Given audit logs exist When an authorized admin queries or exports them Then entries are append-only, time-ordered, filterable by household/statement/action/date, and exportable; attempts to modify or delete entries are denied and logged
Export Events (PDF/CSV) Access Control and Logging
Given a user has export permission and access to statement S When they export S as PDF or CSV Then the file is generated and returned with HTTP 200 and contains only scoped statement data Given a user without export permission or without access to S When they attempt to export S Then the system returns HTTP 403 and logs the attempt Given any export of S occurs When the export completes or fails Then an audit log entry is created with {actor id or link id, action 'export', format 'pdf'|'csv', household id, statement id, timestamp, outcome 'success'|'failure'} Given an exported file is produced When its metadata is inspected Then it contains no hidden sheets, tracked changes, or embedded PII beyond the statement content
Encryption in Transit and At Rest
Given any request to view, share, or export a statement When the connection is established Then the system enforces HTTPS with TLS 1.2+; insecure requests are redirected or rejected and logged Given statement data, share tokens, and audit logs are stored When storage is inspected Then data is encrypted at rest using industry-standard algorithms with keys managed in a centralized KMS; no plaintext copies exist Given share links are generated When the URL is reviewed Then it contains no PII and only an opaque token which is invalidated upon revocation or expiry
Household-Level Privacy Settings Enforcement
Given household privacy setting 'Mask co-payer personal details' is enabled When a non-admin household member views statement S Then the system masks co-payer names and contact details while leaving amounts and plan status visible Given household privacy setting 'Restrict manager access' is enabled and permitted by community policy When a community manager attempts to view S Then the system returns HTTP 403 and logs the attempt; when disabled, authorized managers can view per role Given household privacy setting 'Require 2FA on all external shares' is enabled When any user creates a share link for S Then the link is created with 2FA required by default and the requirement cannot be downgraded by the sharer

Product Ideas

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

Tap-to-Translate Feed

Auto-translates posts, bills, and reminders per member language, preserving layout. One tap toggles original/translated, lifting read rates for bilingual households.

Idea

Grace Plan Catch-Up

Convert past-due balances into automated payment plans with clear schedules and fees. One click proposes terms; reminders adapt to on-time, missed, or partial payments.

Idea

Portfolio Switchboard

One pane to broadcast announcements, clone bills, and track KPIs across communities. Bulk actions with safeguards and per-community overrides cut repetitive work.

Idea

Pay-by-Text Quicklink

Send secure, expiring SMS links that open Apple Pay/Google Pay for one‑tap dues without login. Magic links log activity to the unit’s ledger.

Idea

ARC Snapshot Checklist

Template-driven requests require photos, lot map, and material details before submission. Mobile camera prompts and completeness checks slash back‑and‑forth.

Idea

Neighbor-Safe Redaction

Auto-blur faces, license plates, and house numbers on violation photos before sharing. One toggle reveals originals for authorized reviewers.

Idea

Household Co-Payers

Invite co-payers (tenant, spouse) with limited permissions. Split dues, separate receipts, and individual reminders while keeping one unit ledger.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

Duesly Launches Multilingual Communications Suite to Lift Read Rates and Trust Across Diverse HOAs

Imagined Press Article

Duesly today announced the Multilingual Communications Suite, a set of integrated capabilities that help volunteer boards and part‑time managers keep every resident informed—no matter their preferred language—while preserving formatting, auditability, and trust. The suite brings together Smart Language Detect, Layout Lock, Glossary Guard, Dual View, Reply Translate, Multilingual Nudges, and a Translation Ledger, all inside Duesly’s lightweight HOA management platform. Volunteer boards and small portfolio managers often serve communities where English, Spanish, and other languages are spoken across households. Traditional email blasts and PDFs routinely break formatting in translation, risking confusion over dates, amounts, and legal terms. Duesly’s suite addresses that pain by merging announcements, bills, and compliance conversations into one clean feed where language barriers quietly disappear—and audit trails stay intact. “Clarity is a prerequisite for timely payments and neighborly compliance,” said Duesly’s head of product. “By automating translation while preserving pixel‑perfect layouts and logging what changed, we help boards communicate confidently and residents act quickly without guesswork.” The Multilingual Communications Suite works across the full lifecycle of community messaging: - Smart Language Detect automatically sets each member’s translation language from device settings and past behavior, with an easy per‑post toggle. Members immediately see content in the language they actually read—no painful onboarding. - Layout Lock preserves original formatting, tables, invoice line items, and images exactly, so amounts, due dates, and instructions remain unambiguous and trustworthy in translation. - Glossary Guard keeps HOA names, legal phrases, fees, and acronyms consistent or untranslated based on admin‑defined rules—preventing costly misinterpretations. - Dual View lets readers flip between original and translated content side‑by‑side or stacked, building confidence for bilingual households and simplifying dispute checks. - Reply Translate ensures comments and DMs display in the sender’s language while arriving in the recipient’s, keeping ARC discussions and violation follow‑ups clear. - Multilingual Nudges deliver push, email, and SMS reminders in each member’s preferred language with smart fallbacks if none is set—lifting open rates and on‑time dues. - Translation Ledger records the when/what/how of every translation—engine used, glossary hits, and user toggles—linked to the post or bill for a defensible audit trail. “Before Duesly, we’d paste translations into long email threads and still field questions about amounts,” said a Board Admin Champion from a diverse condo association. “Now our dues notices and compliance posts arrive in each resident’s language with the original visible on tap. Read rates are up, and the confusion is gone.” For residents, the improvements are immediate. “I can toggle Spanish and English in one tap,” said a resident who prefers Spanish updates. “The bill looks the same in both languages, so I know the numbers are right—and I can pay in seconds from my phone.” Why it matters: Payment and compliance outcomes depend on comprehension. When residents fully understand what’s due, by when, and why, on‑time rates climb and neighbor friction falls. The Multilingual Communications Suite reduces the cognitive overhead of translation while eliminating the formatting drift that undermines trust. How it works: Admins create announcements and bills as usual in Duesly’s feed. The platform automatically detects recipient language preferences, applies glossary rules, and renders the translation with Layout Lock. Dual View and per‑post toggles give control back to the reader. When replies arrive, Reply Translate handles both directions without extra steps. Nudges across channels inherit the same language logic. Every action is logged to the Translation Ledger with timestamps for audit clarity. Governance and privacy: Glossary Guard and the Translation Ledger give boards and bookkeepers confidence that official terms remain consistent and defensible. Exportable logs attach to the post or bill, and reviewers can confirm original/translated content with Dual View during approvals. Availability: The Multilingual Communications Suite begins rolling out today to new and existing Duesly communities. Admins can enable it in Settings and define glossary terms in minutes. The features work on web and mobile, and recipients do not need to create an account to read and pay from secure links. Getting started: Boards and managers can migrate existing templates into Duesly and set common legal and financial phrases in the glossary. For help optimizing translations and glossary entries, Duesly offers a quick start guide within the app. About Duesly: Duesly is a lightweight HOA management platform for volunteer boards and part‑time managers at small and mid‑size communities. It merges announcements, payments, and compliance into one clean feed, enabling one‑click post‑to‑bill, automated reminders, and digital payments that lift read rates and on‑time dues. Media contact: press@duesly.com Press kit and demos: www.duesly.com/press

P

Duesly Unveils Portfolio Switchboard for Part‑Time Managers: Faster Broadcasts, Safer Bulk Actions, Clear KPIs

Imagined Press Article

Duesly today introduced Portfolio Switchboard, a suite of tools that gives part‑time and portfolio HOA managers a single pane to broadcast announcements, clone bills, govern bulk actions, and track results across communities—without enterprise overhead. The launch brings Segmented Broadcasts, Adaptive Clone, KPI Heatmap, Bulk Guardrails, Override Panel, and Wave Scheduler together in one streamlined workflow. Part‑time portfolio managers juggle multiple small associations with limited time and high expectations. Re‑creating messages and bills community by community leads to copy‑paste errors, date collisions, and noisy, irrelevant messages that reduce trust. Duesly’s Portfolio Switchboard solves that by centralizing targeting, validation, and scheduling while preserving per‑community nuance and audit trails. “Managing five or fifteen communities doesn’t have to mean five or fifteen times the work,” said Duesly’s head of product. “With Portfolio Switchboard, managers compose once, target precisely, run a safety check, and roll it out in waves—while still fine‑tuning the details that matter in each association.” Portfolio Switchboard includes: - Segmented Broadcasts: Build dynamic audiences across communities using filters like dues status, roles, language, buildings, and tags. See live recipient counts, preview who’s in or out, and simulate delivery channels to avoid over‑messaging. - Adaptive Clone: Replicate announcements and bills across communities while auto‑adapting amounts, due dates, GL codes, and fee policies to each community’s settings. Tokenized templates fill in names and contacts; per‑community diffs preview changes before launch. - KPI Heatmap: Visualize read rates, payment completion, delinquency shifts, and reminder effectiveness at a glance across the portfolio. Color‑coded tiles and trend lines highlight outliers; click to drill into segments or export for reporting. - Bulk Guardrails: Run a preflight dry‑run validation before any bulk action. Catch duplicates, date collisions, missing ledger codes, audience conflicts, and other risks. Require approvals for large sends, sample a small cohort first, and keep a one‑click rollback with versioned change logs. - Override Panel: Apply a global action, then fine‑tune per community in one side‑by‑side view. Edit amounts, dates, channels, and audiences inline; exempt a community; or save an exception as a reusable rule. Every override records who and why. - Wave Scheduler: Respect time zones and quiet hours with timed waves. Rate‑limit to avoid support spikes, auto‑pause on bounces or error thresholds, auto‑retry failed deliveries, and A/B test the first wave before expanding the winner automatically. “Portfolio Switchboard let us send one dues notice across nine HOAs with correct dates and GL codes for each,” said a part‑time portfolio manager who piloted the suite. “The dry‑run caught a collision we would’ve missed, and the heatmap showed which community needed an extra nudge.” Why it matters: Residents trust clear, relevant messages. When communications are targeted and consistent—and when bills match each community’s policies—questions go down and on‑time rates go up. For managers, the gain is time and confidence: fewer manual edits, fewer mistakes, and easy rollbacks if something slips. How it works: Managers compose content in the Duesly feed, choose cross‑community segments, and preview recipients and delivery channels. Adaptive Clone generates per‑community variants and presents diffs. Bulk Guardrails performs a validation pass, and, once approved, Wave Scheduler stages delivery respecting quiet hours. The KPI Heatmap starts tracking outcomes immediately after sends, providing insight for follow‑ups. Governance and audit: Every bulk action is versioned with change logs and approvals. The Override Panel and Bulk Guardrails record who approved what and why, keeping portfolios compliant with internal policies and ready for audits. Availability: Portfolio Switchboard begins rolling out today to Duesly customers managing more than one community. Interested boards and managers can request access within Settings. All features operate on web and mobile, and residents never need to log in to receive announcements or pay from secure links. Onboarding support: Duesly provides templated broadcasts and bill templates to accelerate adoption, plus best‑practice guides for targeting, wave timing, and KPI thresholds that surface at‑risk communities. About Duesly: Duesly is a lightweight HOA management platform for volunteer boards and part‑time managers at small and mid‑size communities. It merges announcements, payments, and compliance into one clean feed. Turn any post into a bill with one click, trigger automated, logged reminders, and replace email chaos and paper checks—lifting read rates and on‑time dues. Media contact: press@duesly.com Press kit and demos: www.duesly.com/press

P

Duesly Introduces Automated Payment Plans to Reduce Delinquencies and Replace Paper Checks

Imagined Press Article

Duesly today launched an integrated suite for automated payment plans that helps volunteer boards and treasurers convert past‑due balances into clear, consistent installment schedules—without spreadsheets or long email exchanges. The release combines Auto Terms Builder, Plan Autopay, Smart Reflow, Self‑Serve Offers, Fairness Guardrails, and Fee Clarity, all designed to stabilize cash flow, reduce delinquencies, and give residents a simple, dignified path to catch up. Small and mid‑size associations often rely on one treasurer juggling reconciliations, paper checks, and difficult conversations with late payers. Inconsistent terms and manual tracking create confusion and disputes that delay recovery. Duesly’s approach standardizes policy, automates reminders and receipts, and shows members exactly what they owe and when in plain language. “Communities need both compassion and consistency,” said Duesly’s general manager for payments. “We’ve made it possible to offer fair, policy‑aligned plans in seconds, then keep them on track automatically with autopay, smart retries, and clear receipts. Treasurers get audit‑ready records without extra work.” The payment plan suite includes: - Auto Terms Builder: One click proposes a compliant payment plan tailored to the member’s balance, age of debt, and community policy. Admin‑set rules—minimum payment, maximum duration, fees and interest, and start date windows—auto‑populate a clear schedule and message that’s consistent and defensible. - Plan Autopay: Members can enable automatic installment payments via ACH or card so they never miss a due date. Smart retries, instant receipts, and transparent status updates keep everyone informed while lifting on‑time rates. - Smart Reflow: If a payment is early, partial, or missed, the plan recalculates remaining installments within admin guardrails—extending the end date or adjusting amounts as configured. The ledger logs every change for audit clarity. - Self‑Serve Offers: Members can propose their own plan with simple sliders for amount, due day, and start date. If the offer fits policy, it auto‑approves; if not, it routes to admins with a prefilled rationale and optional hardship notes. - Fairness Guardrails: Boards define eligibility and consistency rules—minimum first payment, fee‑waiver conditions while current, maximum plans per year, and cooldowns after default. Duesly enforces them automatically and records exceptions with reasons. - Fee Clarity: A plain‑language breakdown shows principal, fees, and any interest, plus a side‑by‑side total cost comparison for paying now vs. on the plan. Members can tap to expand an amortization view and accept terms digitally. “As a volunteer treasurer, consistency matters,” said a board treasurer who piloted the suite. “Auto Terms Builder removed debate from the process, and the receipts plus audit logs mean we can answer questions quickly. The result is fewer awkward conversations and faster recoveries.” Why it matters: Predictable cash flow fuels reliable maintenance and amenities. When residents see a clear, fair path to catch up—and can opt into autopay with confidence—delinquencies drop and administrative effort falls. How it works: Admins select a delinquent balance and click “Offer Plan.” Auto Terms Builder applies policy to generate terms, which can be sent immediately. Residents review Fee Clarity details, accept digitally, and optionally toggle Plan Autopay. If life happens, Smart Reflow adjusts within rules and communicates the updated schedule. Self‑Serve Offers empowers residents to propose terms aligned with policy—streamlining negotiation. Governance and audit: Every plan includes immutable logs of offers, acceptances, changes, retries, and communications. Fairness Guardrails ensure equal treatment across households, and any manual exceptions require a reason that’s recorded. Availability: Automated payment plans are available starting today to new and existing Duesly communities. Admins can configure rules in Settings and begin offering plans immediately from any past‑due bill or balance. Residents can enroll from web or mobile without creating an account. Getting started: Duesly provides a quick policy template and sample messages, along with guidance for fee disclosure and plan length that balances compassion and community needs. Treasurers can export plan summaries and receipts for bookkeeping with a single click. About Duesly: Duesly is a lightweight HOA management platform for volunteer boards and part‑time managers at small and mid‑size communities. It merges announcements, payments, and compliance into one clean feed, enabling one‑click post‑to‑bill, automated reminders, and digital payments that replace email chaos and paper checks. Media contact: press@duesly.com Press kit and demos: www.duesly.com/press

P

Duesly Debuts Secure Pay‑by‑Text Quicklinks with One‑Tap Wallets to Boost On‑Time Dues

Imagined Press Article

Duesly today announced a major upgrade to mobile payments for homeowner associations: secure Pay‑by‑Text quicklinks that open Apple Pay or Google Pay automatically, keep balances in sync in real time, and protect against forwarded links and misapplied payments. The launch combines Forward Lock, Wallet SmartOpen, Live Balance Sync, Split Quicklink, Link Rescue, Receipt Backtext, and One‑Tap Autopay to reduce friction for residents and workload for treasurers. Communities rely on timely dues, but payment experiences often force app logins, account creation, or manual amount entry that lead to drop‑offs and errors. Duesly’s approach meets members where they are—on their phones—with a secure, device‑aware link that takes them straight to a trusted wallet or a saved method in seconds. “Pay‑by‑Text should be as simple as a tap and as safe as your bank,” said Duesly’s head of product. “By binding links to devices, opening native wallets by default, and keeping balances live, we’ve removed the biggest sources of friction and error for communities.” What’s new: - Forward Lock: Every Pay‑by‑Text link is one‑time, device‑bound, and time‑limited. If a link is forwarded or opened from a new device, it safely gates with a quick SMS code or unit check. Admins set expiry windows and open limits, and Duesly logs every attempt for audit clarity. - Wallet SmartOpen: Duesly detects the recipient’s device and launches Apple Pay or Google Pay automatically with the amount, memo, and due date prefilled. If no wallet is available, the flow falls back to a secure card/ACH screen or a saved method from prior payments. - Live Balance Sync: Quicklinks always reflect the latest balance, credits, fees, and partials in real time. If someone pays from another channel or a late fee posts, the link updates before checkout, with clear options like “Minimum Due” or “Pay in Full.” - Split Quicklink: Send separate quicklinks to co‑payers (spouse, tenant, roommate) with a defined split or pay‑what‑you‑can targets. Each link shows the payer’s portion and the live remaining balance; receipts are individual while the unit ledger stays unified. - Link Rescue: Duesly continuously monitors SMS deliverability and auto‑regenerates a fresh magic link if a message bounces or expires. It can fall back to email/push and escalate to a voice read‑out code for critical dues—respecting time zones and quiet hours. - Receipt Backtext: After payment, members receive an instant text confirmation with amount, timestamp, last‑4 of the method, and a reference ID, plus a link to a downloadable PDF receipt and an add‑to‑calendar nudge for the next due. - One‑Tap Autopay: Post‑payment, Duesly offers a single toggle to save the wallet and enroll in autopay for future dues—no account creation required. Terms, reminder cadence, and cancel links arrive in the confirmation SMS. “For our on‑the‑go residents, the difference is night and day,” said a Board Treasurer at a townhome association using the upgrade. “Payments clear faster, questions dropped, and we’ve stopped chasing paper checks.” A resident’s perspective underscores the speed: “I tapped the link, Face ID completed Apple Pay, and I was done in seconds,” said a homeowner who prefers mobile wallets. “The receipt text with a calendar reminder was a great touch.” Why it matters: Friction in the last mile costs communities cash flow and trust. By letting members pay instantly without passwords, while maintaining strong device‑level safeguards and audit trails, Duesly delivers a best‑of‑both‑worlds experience: faster completes and fewer mistakes. How it works: Admins can convert any post to a bill with one click. Duesly generates Pay‑by‑Text quicklinks that respect Forward Lock policies and Live Balance Sync. Recipients tap to pay with Wallet SmartOpen or a saved method, receive Receipt Backtext immediately, and are offered One‑Tap Autopay for next time. For shared households, Split Quicklink coordinates individual payments while keeping the unit ledger unified. Security and audit: Every link attempt, device check, and balance sync is logged. Admins set expiry windows, open limits, and channel fallbacks in Settings, and exports include reference IDs and confirmation data for reconciliation. Availability: Pay‑by‑Text quicklinks and wallet flows begin rolling out today to Duesly communities on web and mobile. No download or account creation is required for residents to pay securely. Getting started: Boards can enable Pay‑by‑Text in Settings, configure Forward Lock, and send a sample to themselves in minutes. Duesly provides prewritten message templates and best practices for timing and channel mix. About Duesly: Duesly is a lightweight HOA management platform for volunteer boards and part‑time managers at small and mid‑size communities. It merges announcements, payments, and compliance into one clean feed, replacing email chaos and paper checks with higher read rates and on‑time dues. Media contact: press@duesly.com Press kit and demos: www.duesly.com/press

P

Duesly Rolls Out Neighbor‑Safe ARC and Compliance Workflow with Auto‑Redaction and Review‑Ready Packets

Imagined Press Article

Duesly today unveiled a modern, privacy‑first workflow for architectural requests (ARC) and compliance that helps residents submit complete, review‑ready packets while protecting sensitive details in photos and documents. The release unites Guided Capture, Plot Overlay, Spec Picker, Neighbor Acknowledgment, Readiness Check, and Review Pack with a powerful privacy layer featuring Smart Auto‑Mask, Confidence Heatmap, Timed Reveal, Parcel Guard, Proof Stamp, Brush Assist, and Bulk Redactor. ARC and compliance processes are often slow and contentious because submissions lack required photos, measurements, or materials—and because images can inadvertently expose faces, license plates, or neighboring homes. Duesly addresses both issues by guiding residents to collect the right information the first time and automatically redacting sensitive details before any sharing occurs. “Our goal is a neighbor‑friendly process that’s fast, complete, and privacy‑aware,” said Duesly’s head of product. “We help applicants submit a solid packet on the first try, give reviewers exactly what they need, and ensure sensitive details are masked with audit‑ready proof.” Submission made simple: - Guided Capture: Step‑by‑step camera prompts ensure every required photo is taken correctly—front, side, street view, and setbacks. Auto‑labels, timestamps, and location metadata attach to each shot. - Plot Overlay: Applicants can trace the project footprint on a satellite or parcel map, snapping to lot lines and dropping dimensions and setback distances. Reviewers instantly see placement and potential encroachments. - Spec Picker: Project‑specific templates (fence, paint, roof, solar) gather exact materials, colors, finishes, and manufacturer references to align with CC&Rs. - Neighbor Acknowledgment: Required adjacent‑owner acknowledgments are requested via SMS/email with a concise project summary and thumbnails. Duesly tracks who’s signed and logs consent for the packet. - Readiness Check: Real‑time completeness and policy validation flags missing photos, map traces, dimensions, or off‑policy selections before submission, with clear fixes and inline tips. - Review Pack: One click compiles an exportable packet—cover summary, captioned photos, plot overlay, specs, and signatures. Optional redaction protects personal details for neighbor sharing while preserving a full audit copy. Privacy by default: - Smart Auto‑Mask: Faces, license plates, and house numbers are detected and blurred automatically the moment photos are captured or uploaded. Tunable strength and styles (blur, pixelate, box) ensure sensitive details stay private without extra steps. - Confidence Heatmap: Color cues overlay each image to show high and low detection confidence, guiding quick manual touch‑ups where needed. - Timed Reveal: Authorized reviewers can temporarily view originals behind a secure gate with a required reason, automatic expiry, and watermarked overlays. Every reveal is logged and auto‑re‑masked. - Parcel Guard: Photos are auto‑cropped to the property boundary using GPS and parcel maps, trimming backgrounds that often include neighbors or unrelated homes. - Proof Stamp: A tamper‑evident seal links each redacted image to its original, mask coordinates, and timestamp. Exports include a verification hash and activity log to prove what was hidden—and only that. - Brush Assist and Bulk Redactor: Fast manual tools snap masks to edges for tricky reflections or signage, and batch redaction accelerates portfolio workflows with consistent settings and an easy rollback. “Reviewers used to send me back for different angles or details,” said a homeowner who recently submitted a paint and fence request. “Duesly told me exactly what to capture, and the map overlay made placement obvious. I also felt better sharing photos knowing plates and faces were blurred automatically.” From the committee side, the experience is calmer. “We can make decisions faster because packets are consistent and complete,” said a Compliance Monitor from a self‑managed HOA. “Timed Reveal and Proof Stamp give us confidence to investigate when needed without increasing privacy risk.” Why it matters: Faster, clearer ARC decisions reduce project delays and neighbor disputes; robust privacy reduces friction and legal exposure. When both improve, volunteer time is respected and community trust rises. How it works: Applicants start from the Duesly feed, select a project template, and follow Guided Capture prompts. Auto‑masking runs on device and in the cloud, while Readiness Check validates completeness and policy alignment. Neighbor Acknowledgment requests are sent automatically. Reviewers receive a Review Pack ready for decision, with audit logging and optional reveals for deeper inspection. Availability: The ARC and privacy workflow launches today across web and mobile for all Duesly communities. Admins can customize required photos, specs, and acknowledgment rules per project type, and set default redaction styles. Getting started: Boards can enable the ARC module in Settings and apply sample templates immediately. Duesly provides a quick‑start guide and best practices for common requests, including fences, exterior paint, roofs, and solar. About Duesly: Duesly is a lightweight HOA management platform for volunteer boards and part‑time managers at small and mid‑size communities. It merges announcements, payments, and compliance into one clean feed to lift read rates and on‑time dues while reducing email chaos and paper checks. Media contact: press@duesly.com Press kit and demos: www.duesly.com/press

P

Duesly Adds Household Co‑Payers with Smart Splits, Nudge Sync, and Backstop Autopay to End Last‑Minute Chasing

Imagined Press Article

Duesly today released Household Co‑Payers, a set of capabilities that lets boards invite tenants, spouses, roommates, and other contributors to pay their share securely—without exposing private information or creating extra work for treasurers. The suite includes Flex Split, Backstop Autopay, Nudge Sync, Role Presets, Split Rebalance, Guest Pay Pass, and a monthly Household Statement that clarifies who paid what and what remains. Shared households are common across associations, but most payment systems assume a single payer. That mismatch leads to last‑minute scrambling, forwarded links, and uncomfortable group texts. Duesly’s approach reflects how households actually coordinate bills: multiple payers, different methods, partial payments at different times—managed automatically against a single unit ledger. “Collections shouldn’t depend on perfect coordination inside a household,” said Duesly’s head of product. “We let communities set clear rules for splits and permissions, then automate the nudges and safety nets so dues get paid on time without extra chasing.” Household Co‑Payers includes: - Flex Split: Boards or owners can set household split rules by percent, fixed amount, or up‑to caps—plus optional minimums. Duesly applies the rules to every bill automatically so each co‑payer sees their exact share and the live remaining balance. - Backstop Autopay: Owners can enable a safety net so if a co‑payer hasn’t covered their share by the due date (or a grace window), Duesly auto‑runs an owner backstop for “whatever remains.” Receipts show who paid which portion and when, preventing late fees and last‑minute chasing. - Nudge Sync: Reminders are coordinated across co‑payers so the right person gets the right nudge at the right time. If one pays, Duesly quiets prompts for the rest; if neither pays, it escalates gently to both via their preferred channels. - Role Presets: Invite co‑payers with simple, safe permission templates—Tenant, Roommate, Co‑Owner, Payer‑Only. Control what they can see (balances, notices), what they can do (pay, set autopay, view receipts), and which methods they can use, with one‑tap revoke and an audit trail for every change. - Split Rebalance: When fees, credits, or partials land mid‑cycle, Duesly automatically recalculates each person’s share based on split rules and payment timing. It resolves over/underpayments by applying proportional credits or optional refunds to the correct payer. - Guest Pay Pass: Create a time‑limited, amount‑capped payment pass for a one‑time contributor (like a visiting parent or new roommate) without granting account access. Device‑bound, expiring links preserve privacy and tag the receipt to the guest. - Household Statement: A clean monthly roll‑up shows total due, who paid what, what remains, any fees, and plan status by co‑payer. Shareable as PDF/CSV for landlords, accountants, or reimbursement needs, it ends “who covered this?” threads and builds trust inside the household. For landlords and shared units, the impact is immediate. “I invite tenants as Payer‑Only and set a 70/30 split,” said a remote landlord who manages from afar. “If they miss, Backstop Autopay covers the gap so we avoid late fees. The statement makes reimbursements simple.” Residents feel more in control, too. “Seeing my exact portion and getting my own reminders reduced stress,” said a co‑payer in a townhome. “I paid from my phone and didn’t need access to the full account.” Why it matters: Clear responsibility and coordinated reminders reduce missed payments and household friction. Treasurers gain predictable cash flow and fewer support tickets; residents gain transparency and privacy. How it works: Admins or owners define split rules once in Duesly. Bills inherit the rules automatically, and Split Quicklinks (when enabled) deliver each person’s portion by text or email. Nudge Sync orchestrates reminders, and Backstop Autopay runs only for the remaining balance if needed. Role Presets control access for each co‑payer and guest, and every action is logged. Governance and privacy: Permissions are explicit, revocable, and audited. Individual receipts protect privacy by showing only the payer’s portion while preserving a unified unit ledger for the association. Availability: Household Co‑Payers is available starting today on web and mobile for all Duesly communities. Boards and owners can configure roles and splits in Settings and invite co‑payers in minutes. Getting started: Duesly provides best‑practice templates for common scenarios—owner plus tenant, spouses with different paydays, and shared student housing—along with messaging that sets expectations and reduces confusion. About Duesly: Duesly is a lightweight HOA management platform for volunteer boards and part‑time managers at small and mid‑size communities. It merges announcements, payments, and compliance into one clean feed, enabling one‑click post‑to‑bill, automated reminders, and digital payments that lift read rates and on‑time dues. Media contact: press@duesly.com Press kit and demos: www.duesly.com/press

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.